PST-008: The single sign-on handshake can be started by a cross-site GET (SOCIALACCOUNT_LOGIN_ON_GET) #737

Closed
opened 2026-10-10 10:55:47 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-008. Severity: Low.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Low
  • Category: login-csrf / state-changing-GET
  • Location: src/postulo/config/settings/base.py (331-334; 331-333; 333)
  • Found by: CSRF, methods and redirects, authentication and sessions, configuration and dependencies, secrets and cryptography

Who can do it, and what they need. A hostile page the victim visits while signed in to Postulo (for process=connect) or while signed out (for process=login). For account linking to benefit the attacker, the operator's identity provider must itself be vulnerable to login CSRF or must auto-approve with a session the attacker controls; otherwise the flow links the victim's own identity, which is harmless. …

How it works.

  1. Attacker embeds <img src="https://postulo.example/accounts/sso/oidc/login/?process=connect"> or a link.
  2. Because LOGIN_ON_GET is True, allauth's OAuth2LoginView performs the redirect to the IdP immediately on a top-level GET (cookies are sent under SameSite=Lax for top-level navigations), with no POST and therefore no CSRF token.
  3. If the IdP session in the victim's browser is one the attacker established (IdP login CSRF), the callback connects the attacker's IdP identity to the victim's Postulo account; the attacker can then sign in as the victim through SSO.
  4. Independently, every such GET makes the Postulo server fetch the OIDC discovery document through allauth's requests.Session() (socialaccount/adapter.py:344-347), not through postulo.plugins.http.client(); scratch test showed an unauthenticated GET to /accounts/sso/oidc/login/ immediately attempting an outbound HTTPS request.

Impact. Account linking by a cross-site GET when the IdP is weak; unauthenticated callers can make the server issue one outbound request per hit to the configured IdP (destination is operator-configured, so not SSRF, but it bypasses the project's guarded client and its destination policy).

Evidence.

base.py:331-333 `# The provider round-trip carries its own state; a plain link avoids the content security policy (form-action 'self') silently blocking the redirect a POST form would make.` / `SOCIALACCOUNT_LOGIN_ON_GET = True`; allauth default is False (socialaccount/app_settings.py:142-143 `return self._setting("LOGIN_ON_GET", False)`). Scratch test output: `sso login GET` raised a ProxyError from `requests` while fetching the discovery document.

Verification (verdict: confirmed; severity after verification: Low). Keep low, close to informational. The real precondition for account linking is an IdP-side login-CSRF or auto-approve weakness, which is an operator-chosen trust item. The outbound-fetch amplification exists with or without LOGIN_ON_GET, needs no extra precondition beyond an extra GET and POST, and is a DoS-class concern the threat model puts out of scope. Treat 'allauth's HTTP session bypasses the guarded client' as informational. The path is operator-configured and the IdP is trusted. The guard would break LAN identity providers. Remediation options: keep the setting and document the trade-off, or switch to POST and add the IdP origin to form-action when SSO is configured. Rate-limiting the unauthenticated entry is optional. Merge with #90.

Suggested remediation. Set SOCIALACCOUNT_LOGIN_ON_GET back to allauth's default (False) and make the SSO button a POST form; the CSP concern can be met by listing the IdP's authorize origin in form-action (or by answering the POST with a 303 to the IdP, which form-action 'self' permits in Firefox/Safari and which Chrome also allows for the initial POST target being self when the redirect is followed from a non-form navigation) rather than by opening the flow to GET. Document that the IdP must enforce its own login-CSRF protection. Consider caching the discovery document or routing allauth's HTTP through the guarded client.

Also covered by this issue.

  • SOCIALACCOUNT_LOGIN_ON_GET enables starting the OIDC flow from a plain link (src/postulo/config/settings/base.py (331-333); Low). Classic login-CSRF through the provider; low here because the instance has one operator-chosen IdP and allauth's state parameter still binds the callback to the session that started it. Accounts dimension overlap; recorded because it is a settings default.
  • The SSO handshake can be started by a cross-site GET (SOCIALACCOUNT_LOGIN_ON_GET) (src/postulo/config/settings/base.py (331-333); Informational). Hardening gap acknowledged in allauth's own documentation; the project chose GET because form-action 'self' in the CSP blocks the redirect of a POSTed form. No concrete account compromise was found.
  • SOCIALACCOUNT_LOGIN_ON_GET is enabled, letting a third-party page start the SSO redirect without a user gesture (src/postulo/config/settings/base.py (333); Informational). Minor: drive-by initiation of SSO; no account linking without the victim completing the provider step.
Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-008**. Severity: **Low**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Low - **Category:** login-csrf / state-changing-GET - **Location:** `src/postulo/config/settings/base.py` (331-334; 331-333; 333) - **Found by:** CSRF, methods and redirects, authentication and sessions, configuration and dependencies, secrets and cryptography **Who can do it, and what they need.** A hostile page the victim visits while signed in to Postulo (for `process=connect`) or while signed out (for `process=login`). For account linking to benefit the attacker, the operator's identity provider must itself be vulnerable to login CSRF or must auto-approve with a session the attacker controls; otherwise the flow links the victim's own identity, which is harmless. … **How it works.** 1. Attacker embeds `<img src="https://postulo.example/accounts/sso/oidc/login/?process=connect">` or a link. 2. Because LOGIN_ON_GET is True, allauth's OAuth2LoginView performs the redirect to the IdP immediately on a top-level GET (cookies are sent under SameSite=Lax for top-level navigations), with no POST and therefore no CSRF token. 3. If the IdP session in the victim's browser is one the attacker established (IdP login CSRF), the callback connects the attacker's IdP identity to the victim's Postulo account; the attacker can then sign in as the victim through SSO. 4. Independently, every such GET makes the Postulo server fetch the OIDC discovery document through allauth's `requests.Session()` (socialaccount/adapter.py:344-347), not through `postulo.plugins.http.client()`; scratch test showed an unauthenticated GET to `/accounts/sso/oidc/login/` immediately attempting an outbound HTTPS request. **Impact.** Account linking by a cross-site GET when the IdP is weak; unauthenticated callers can make the server issue one outbound request per hit to the configured IdP (destination is operator-configured, so not SSRF, but it bypasses the project's guarded client and its destination policy). **Evidence.** ```text base.py:331-333 `# The provider round-trip carries its own state; a plain link avoids the content security policy (form-action 'self') silently blocking the redirect a POST form would make.` / `SOCIALACCOUNT_LOGIN_ON_GET = True`; allauth default is False (socialaccount/app_settings.py:142-143 `return self._setting("LOGIN_ON_GET", False)`). Scratch test output: `sso login GET` raised a ProxyError from `requests` while fetching the discovery document. ``` **Verification** (verdict: confirmed; severity after verification: Low). Keep low, close to informational. The real precondition for account linking is an IdP-side login-CSRF or auto-approve weakness, which is an operator-chosen trust item. The outbound-fetch amplification exists with or without LOGIN_ON_GET, needs no extra precondition beyond an extra GET and POST, and is a DoS-class concern the threat model puts out of scope. Treat 'allauth's HTTP session bypasses the guarded client' as informational. The path is operator-configured and the IdP is trusted. The guard would break LAN identity providers. Remediation options: keep the setting and document the trade-off, or switch to POST and add the IdP origin to form-action when SSO is configured. Rate-limiting the unauthenticated entry is optional. Merge with #90. **Suggested remediation.** Set SOCIALACCOUNT_LOGIN_ON_GET back to allauth's default (False) and make the SSO button a POST form; the CSP concern can be met by listing the IdP's authorize origin in `form-action` (or by answering the POST with a 303 to the IdP, which `form-action 'self'` permits in Firefox/Safari and which Chrome also allows for the initial POST target being self when the redirect is followed from a non-form navigation) rather than by opening the flow to GET. Document that the IdP must enforce its own login-CSRF protection. Consider caching the discovery document or routing allauth's HTTP through the guarded client. **Also covered by this issue.** - **SOCIALACCOUNT_LOGIN_ON_GET enables starting the OIDC flow from a plain link** (`src/postulo/config/settings/base.py` (331-333); Low). Classic login-CSRF through the provider; low here because the instance has one operator-chosen IdP and allauth's state parameter still binds the callback to the session that started it. Accounts dimension overlap; recorded because it is a settings default. - **The SSO handshake can be started by a cross-site GET (SOCIALACCOUNT_LOGIN_ON_GET)** (`src/postulo/config/settings/base.py` (331-333); Informational). Hardening gap acknowledged in allauth's own documentation; the project chose GET because form-action 'self' in the CSP blocks the redirect of a POSTed form. No concrete account compromise was found. - **SOCIALACCOUNT_LOGIN_ON_GET is enabled, letting a third-party page start the SSO redirect without a user gesture** (`src/postulo/config/settings/base.py` (333); Informational). Minor: drive-by initiation of SSO; no account linking without the victim completing the provider step.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:21 +00:00
Author
Owner

Fixed in d4251b1c6 on main.

Fixed in d4251b1c6 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#737
No description provided.