Summary
A cross-site request forgery / session fixation vulnerability in Vaultwarden's SSO authorization flow allowed an unauthenticated attacker to hijack any SSO user's login and obtain a fully authenticated session, leading to account takeover.
The OAuth state parameter accepted by the authorize endpoint was not bound to the initiating browser session. Combined with a PKCE flow under attacker-controlled parameters and a cleanup gap on failed token exchange, an attacker could pre-create an authorization context, induce a victim to complete IdP authentication against it, and then redeem the resulting tokens themselves.
Affected versions
All Vaultwarden versions with SSO support up to and including 1.35.8. Versions 1.36.0 and up are not vulnerable.
Root cause
Three design decisions combined to produce the vulnerability:
- No session binding at
/connect/authorize. The endpoint accepted arbitrary client-supplied state and code_challenge values without authentication, cookies, or any other browser-session anchor.
SsoAuth keyed by the client-supplied state. The server-side record holding the OIDC flow context was primary-keyed on a value the caller controls, allowing an attacker to fixate a row and have a subsequent IdP callback bearing that state populate it.
- No cleanup of
SsoAuth on failed token exchange. When exchange_code propagated an IdP error (e.g. from a mismatched PKCE verifier), the row was left intact, preserving the victim's authorization code for later redemption.
Attack chain
- The attacker generates a PKCE
code_challenge/code_verifier pair and a state value, then calls /connect/authorize with these. An SsoAuth row is stored keyed by the attacker's state, and the IdP authorization URL is returned.
- The attacker sends the IdP URL to the victim. The victim authenticates with the IdP — if an IdP session is already active, no interaction beyond the click is required.
- The IdP redirects the victim's browser to
/connect/oidc-signin with their real authorization code. The server stores this code in the attacker-fixated SsoAuth row without verifying that the callback originated from the same browser that initiated the flow.
- The victim's browser is redirected to
sso-connector.html, where the Bitwarden client automatically attempts token exchange. Because the client does not hold the attacker's verifier, the exchange fails at the IdP. The SsoAuth row is not deleted.
- The attacker calls
/connect/token with the fixated state (which doubles as code in this implementation) and their own code_verifier. The exchange succeeds, the IdP returns the victim's identity, and access and refresh tokens for the victim's account are issued to the attacker.
Impact
- Full account takeover of any SSO user without two-factor authentication.
- Access to the victim's encrypted vault material (
PrivateKey, Key, ciphers), enabling offline brute-force of the master password.
- Account squatting / identity linking when
SSO_SIGNUPS_MATCH_EMAIL is enabled (the default).
- Disclosure of the victim's email, name, and SSO subject/issuer even when 2FA blocks the final session issuance.
Preconditions
- SSO must be enabled on the Vaultwarden instance.
- The victim must hold (or be able to create, under default settings) a Vaultwarden account linked by email to the IdP identity.
- The victim must authenticate with the IdP when redirected (or already hold an active IdP session).
- 2FA on the victim's account blocks final session issuance but does not prevent the rest of the chain.
Fix
The patched version binds the SSO authorization flow to the initiating browser so that an attacker-initiated SsoAuth row cannot be populated by a different browser's IdP callback. Server-side state cleanup on failed token exchange has also been tightened.
Workarounds for unpatched instances
- Enforce 2FA for all SSO users. Blocks final session issuance but does not prevent the identity leak or account linking under default signup settings.
- Restrict SSO to known domains via
SSO_ALLOWED_ORGS / SIGNUPS_DOMAINS_WHITELIST to limit the targetable user pool.
- Apply rate limits to
/connect/authorize and /connect/token at a reverse proxy layer.
References
- RFC 6749 §10.12 — Cross-Site Request Forgery
- OAuth 2.0 Security Best Current Practice §4.10 — Session Fixation
- OAuth 2.0 for Browser-Based Apps — Login CSRF
Credits
Independently reported between February and April 2026 by:
- @Sijisu (2026-02-28)
- @ddd (2026-04-11)
- @santhsecurity (2026-04-25)
Individual roles are listed in the Credits section of this advisory.
Summary
A cross-site request forgery / session fixation vulnerability in Vaultwarden's SSO authorization flow allowed an unauthenticated attacker to hijack any SSO user's login and obtain a fully authenticated session, leading to account takeover.
The OAuth
stateparameter accepted by the authorize endpoint was not bound to the initiating browser session. Combined with a PKCE flow under attacker-controlled parameters and a cleanup gap on failed token exchange, an attacker could pre-create an authorization context, induce a victim to complete IdP authentication against it, and then redeem the resulting tokens themselves.Affected versions
All Vaultwarden versions with SSO support up to and including 1.35.8. Versions 1.36.0 and up are not vulnerable.
Root cause
Three design decisions combined to produce the vulnerability:
/connect/authorize. The endpoint accepted arbitrary client-suppliedstateandcode_challengevalues without authentication, cookies, or any other browser-session anchor.SsoAuthkeyed by the client-suppliedstate. The server-side record holding the OIDC flow context was primary-keyed on a value the caller controls, allowing an attacker to fixate a row and have a subsequent IdP callback bearing thatstatepopulate it.SsoAuthon failed token exchange. Whenexchange_codepropagated an IdP error (e.g. from a mismatched PKCE verifier), the row was left intact, preserving the victim's authorization code for later redemption.Attack chain
code_challenge/code_verifierpair and astatevalue, then calls/connect/authorizewith these. AnSsoAuthrow is stored keyed by the attacker'sstate, and the IdP authorization URL is returned./connect/oidc-signinwith their real authorization code. The server stores this code in the attacker-fixatedSsoAuthrow without verifying that the callback originated from the same browser that initiated the flow.sso-connector.html, where the Bitwarden client automatically attempts token exchange. Because the client does not hold the attacker's verifier, the exchange fails at the IdP. TheSsoAuthrow is not deleted./connect/tokenwith the fixatedstate(which doubles ascodein this implementation) and their owncode_verifier. The exchange succeeds, the IdP returns the victim's identity, and access and refresh tokens for the victim's account are issued to the attacker.Impact
PrivateKey,Key, ciphers), enabling offline brute-force of the master password.SSO_SIGNUPS_MATCH_EMAILis enabled (the default).Preconditions
Fix
The patched version binds the SSO authorization flow to the initiating browser so that an attacker-initiated
SsoAuthrow cannot be populated by a different browser's IdP callback. Server-side state cleanup on failed token exchange has also been tightened.Workarounds for unpatched instances
SSO_ALLOWED_ORGS/SIGNUPS_DOMAINS_WHITELISTto limit the targetable user pool./connect/authorizeand/connect/tokenat a reverse proxy layer.References
Credits
Independently reported between February and April 2026 by:
Individual roles are listed in the Credits section of this advisory.