Security Glossary

OAuth2 State Fixation

An attack that defeats the OAuth2 state parameter's CSRF protection by fixing a known state value into the victim's session, so a callback carrying the attacker's authorization code passes the state...

OAuth2 State Fixation is an attack that defeats the state parameter's CSRF protection by fixing the value the victim's client will validate against. The attacker plants a state they know into the victim's session before the flow finishes, so a callback the attacker crafts, carrying the attacker's own authorization code, still passes the state check. It is the OAuth analogue of session fixation.

How It Differs From Missing-State CSRF

The OAuth2 State Parameter page covers the plain case: the client generates no state, or never compares it, so the attacker's callback is simply accepted. State fixation is the case where the client does generate and compare state, but stores the expected value somewhere the attacker can write, typically a cookie that is not bound to the authenticated session. The check runs and passes, because the attacker chose both sides of the comparison.

How It Works

  1. The client keeps its expected state in a cookie (for example oauth_state) that is not tied to the server-side session.
  2. The attacker writes a known value into that cookie in the victim's browser, using cookie injection from a sibling subdomain, a separate injection bug, or by getting the victim to start a flow the attacker seeded.
  3. The attacker starts their own OAuth flow and obtains an authorization code bound to the attacker's identity, requesting that same known state.
  4. The attacker delivers the resulting callback URL to the victim.
  5. The victim's client compares the callback state to the fixed cookie value, they match, and it exchanges the attacker's code.

Attack Scenario

// 1. Attacker fixes the victim's state cookie to a known value
//    (e.g. cookie injection from sub.victim-app.com)
Set-Cookie: oauth_state=KNOWN123; Domain=victim-app.com

// 2. Attacker runs their own flow, code is bound to ATTACKER account,
//    state chosen to match the fixed cookie
GET /authorize?response_type=code&client_id=app&state=KNOWN123&...
// -> code=ATTACKER_CODE

// 3. Victim is lured to the crafted callback
https://victim-app.com/callback?code=ATTACKER_CODE&state=KNOWN123

// 4. Client checks state == oauth_state cookie -> KNOWN123 == KNOWN123 -> passes
//    It exchanges ATTACKER_CODE and links the victim's session
//    to the attacker's identity provider account.

The payoff is forced account linking: the victim's logged-in account is now connected to the attacker's Google/GitHub identity, so the attacker can later sign in as the victim. If the callback drives login rather than linking, the victim is silently signed into the attacker's account and any data they enter lands in it.

Why It Works

  • The expected state lives in a client-writable cookie, not in the server-side session, so the attacker can set it.
  • The comparison is state-to-cookie only; nothing ties the value to the specific authenticated user.
  • A missing __Host- prefix or an over-broad Domain lets a subdomain overwrite the cookie.

Prevention

  • Store the expected state in the server-side session keyed to the authenticated user, not in a cookie the browser (or attacker) can write.
  • If state must be carried in a cookie, HMAC it to the session ID and reject any state whose binding does not match the current session.
  • Use the __Host- cookie prefix with Secure and SameSite so a sibling subdomain cannot fix the value.
  • Generate a fresh state per authorization request and invalidate it once consumed, so a fixed value cannot be reused.
  • On account linking, require the linking user to re-confirm rather than linking silently on callback.

PentesterLab Exercises

See Also