OAuth Hacking: The Complete Guide to OAuth and OIDC Attacks

The OAuth state Parameter and CSRF

How a missing or weak OAuth state parameter enables login CSRF: forcing the attacker's account onto the victim's session, and why state and PKCE differ.

Published

The state parameter is OAuth's CSRF defense: it binds the callback to the session that started the flow. When a client omits or ignores it, an attacker can force their own account onto the victim. This page covers the attack and why state is not the same as PKCE. Practice on a free live target.

What state binds

At the start of a flow the client generates a random state, stores it in the user's session, and includes it in the authorization request. When the callback returns, the client checks that the returned state matches the one it stored. That check proves the callback belongs to this user's flow and was not injected by someone else. Without it, the client will accept any well-formed callback.

GET /authorize?response_type=code&client_id=app123&redirect_uri=https://app.tld/callback&scope=openid&state=Rk9x8s2Qa HTTP/1.1
Host: auth.example.com

Step 1: test whether state is enforced

Complete a flow, then tamper the returned state and see if the login still succeeds:

GET /callback?code=SplxlOBeZQ&state=THIS_IS_WRONG HTTP/1.1
Host: app.tld
HTTP/1.1 302 Found
Location: /dashboard

A successful login with a wrong (or missing) state means the client does not enforce it. Dropping state entirely is the other test:

/callback?code=SplxlOBeZQ            no state at all; does the client still log in?
/callback?code=SplxlOBeZQ&state=     empty value accepted?

Step 2: the login-CSRF attack

With neither state nor a PKCE binding on the flow (classically a confidential client that uses a client secret and omits state), you link the victim's session to your account:

  1. Start a real "Log in with ExampleID" flow using your own ExampleID account, up to the callback.
  2. Capture the callback URL, which carries your authorization code: https://app.tld/callback?code=YOUR_CODE.
  3. Do not let your own browser complete it. Instead, deliver that URL to the victim (a link, an image, an auto-submitting form).
  4. The victim's browser loads it, the client redeems your code, and binds the victim's session to your account.
<img src="https://app.tld/callback?code=YOUR_CODE" width="0" height="0">

Now the victim is using your account. Anything they save (a payment method, a document, a note) lands in your account, which you log into later and read.

Why state and PKCE differ

They defend different things, so they are not simply interchangeable:

state   binds the CALLBACK to the user's session  -> explicit CSRF defense
PKCE    binds the CODE to the client instance      -> stops code interception, and incidentally blunts login CSRF

They overlap more than people expect. When PKCE is in use, the attacker's delivered code cannot be redeemed because the victim's browser has no matching code_verifier, so the login-CSRF delivery above fails even without state. That is why RFC 9700 lets PKCE cover the CSRF role when the server supports it. The attack therefore lands on flows that enforce neither, classically a confidential client using only a client secret. The safe default is to enforce both.

Edge variants

predictable state     sequential or timestamp-based values an attacker can guess
reused state          the same value across flows, so a captured one stays valid
state not tied to session   generated but never compared to the stored value

Fixing it

  • Generate a high-entropy state per flow, store it in the session, and reject any callback whose state does not match.
  • Do not rely on PKCE to cover CSRF. Enforce both unless you have a specific reason not to.
  • For OIDC, enforce nonce as well, which is the ID-token equivalent of state.

Full guidance is in preventing OAuth attacks. This is the OAuth face of CSRF. Next, leak tokens from the URL in implicit flow token leakage, or try a login CSRF on a free lab.

← Back to OAuth Hacking: The Complete Guide to OAuth and OIDC Attacks