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.comStep 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.tldHTTP/1.1 302 Found
Location: /dashboardA 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:
- Start a real "Log in with ExampleID" flow using your own ExampleID account, up to the callback.
- Capture the callback URL, which carries your authorization code:
https://app.tld/callback?code=YOUR_CODE. - Do not let your own browser complete it. Instead, deliver that URL to the victim (a link, an image, an auto-submitting form).
- 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 CSRFThey 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 valueFixing it
- Generate a high-entropy
stateper flow, store it in the session, and reject any callback whosestatedoes not match. - Do not rely on PKCE to cover CSRF. Enforce both unless you have a specific reason not to.
- For OIDC, enforce
nonceas well, which is the ID-token equivalent ofstate.
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