OAuth PKCE & State Parameter Security: CSRF, Code Interception, and redirect_uri Flaws
The OAuth 2.0 authorization code flow is the backbone of "Sign in with Google/GitHub/Okta" everywhere, yet it is one of the most consistently mis-implemented protocols on the web. Two small pieces of the spec — the state parameter and PKCE (Proof Key for Code Exchange) — exist specifically to close CSRF and code-interception gaps, and they are exactly the parts developers tend to skip, stub out, or validate incorrectly. The result is a recurring set of high-impact findings: account takeover via login CSRF, authorization code theft through a loose redirect_uri, and code injection across clients.
This guide is written for pentesters and bug bounty hunters testing OAuth flows you are authorized to assess. We will walk the authorization code flow, show how to spot and prove each weakness with concrete request manipulation, and finish with the remediation that actually holds up. Throughout, assume you control a browser with an intercepting proxy and a test account on the target — no third parties are being attacked here.
The Authorization Code Flow, Annotated for Attackers
A standard authorization code flow looks like this. The client (relying party) redirects the user's browser to the authorization server, the user authenticates, and the server redirects back with a code the client exchanges for tokens server-to-server.
GET /authorize?response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https://client.example.com/callback
&scope=openid%20profile
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: as.example.com
The two security-critical parameters are state (binds the callback to the user's session, defeating CSRF) and the code_challenge/code_verifier pair (PKCE, binds the code to the client instance that started the flow). When you test a flow, the first thing to do is enumerate which of these are present, whether they are validated, and what happens when you tamper with them. Most real bugs are "the parameter is sent but never checked."
state Parameter Failures: Login CSRF and Account Takeover
The state value must be an unguessable token, tied to the user's pre-auth session, and verified on the callback. If it is missing, static, or unchecked, the flow is vulnerable to login CSRF: an attacker initiates an OAuth flow with their own account, captures the resulting callback URL containing their code, and tricks a victim into visiting it. The victim's client silently links the attacker's identity to the victim's session — now anything the victim does (saving a payment method, uploading files) lands in the attacker's account, or the attacker can later log in as the victim.
Test it methodically:
- Remove it entirely. Strip
statefrom the authorization request and complete the flow. If login still succeeds, there is no CSRF protection. - Replay it. Capture a valid callback, log out, and replay the same
code+statein a fresh session. If accepted,stateis not bound to a session. - Swap it. Take an attacker-generated callback and deliver it to a victim browser. If the victim's account becomes linked, you have account takeover via login CSRF.
- Check entropy. Decode and compare several
statevalues. Sequential, timestamp-derived, or constant values are forgeable.
A delivery PoC is just an auto-submitting page that lands the victim on the attacker's callback URL. You can generate the autosubmit/HTML scaffolding quickly with our CSRF PoC Generator and adapt the action URL to the captured callback.
<!-- Login CSRF: force victim's browser to the attacker's OAuth callback -->
<img src="https://client.example.com/callback?code=ATTACKER_CODE&state=ATTACKER_STATE">
PKCE and Authorization Code Interception
PKCE was designed for public clients (mobile apps, SPAs) that cannot keep a client secret, where a malicious app registered to the same custom URI scheme could intercept the redirect and steal the code. With PKCE, the client generates a random code_verifier, sends its SHA-256 hash as the code_challenge in the authorization request, and presents the raw verifier at the token endpoint. A stolen code is useless without the matching verifier.
# Client generates the verifier and derives the challenge
code_verifier=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -binary -sha256 \
| openssl base64 | tr '+/' '-_' | tr -d '=')
The failure modes are where the findings live:
- Downgrade to
plain. If the server acceptscode_challenge_method=plain, the challenge equals the verifier in cleartext — anyone who sees the authorization request can replay it. Force the method toplainand see if the token endpoint still issues tokens. - PKCE not enforced. Omit
code_challengefrom/authorizeand omitcode_verifierat/token. If you still get tokens, an intercepted code is fully exchangeable. - Verifier not validated. Send a valid challenge but a wrong (or empty) verifier at the token exchange. Acceptance means the binding is decorative.
- Confidential client without PKCE-as-defense-in-depth. Even secret-holding clients benefit; flag flows that rely solely on the secret with no PKCE.
The exchange you want to tamper with looks like this — drop or corrupt code_verifier and watch the response:
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://client.example.com/callback
&client_id=s6BhdRkqt3
&code_verifier=WRONG_OR_EMPTY
redirect_uri Validation Flaws
The redirect_uri is where the authorization code is delivered, so any weakness in how the server matches it can leak the code to an attacker-controlled endpoint. The spec requires exact string matching against pre-registered URIs, but real implementations frequently use prefix matching, allow subpaths, ignore query strings, or normalize URLs in exploitable ways. Combine a redirect_uri flaw with the Referer header or an open redirect on the client and you can siphon codes.
Probe these variations against the registered value https://client.example.com/callback:
- Subdomain/host confusion:
https://client.example.com.attacker.com/callback,https://attacker.com/.client.example.com - Path append (prefix match):
https://client.example.com/callback/../../oauth?x=orhttps://client.example.com/callback.attacker.com - Open redirect chaining: register/use the legitimate URI but route through a client-side open redirect so the code follows the 302 to your host. If the client has a redirect bug, the code lands on your server via the
Locationchain. - Parameter pollution: send two
redirect_urivalues; some servers validate the first and redirect to the second. - Scheme/loopback abuse: for native apps, test
http://localhost:PORTport flexibility and custom-scheme hijacking.
Open redirects are the most common amplifier here. If you find a redirector on the client domain, you can often turn a strict redirect_uri allowlist into a code-exfiltration primitive. Build and fuzz redirect payloads with our Open Redirect Generator, and keep the bypass list from the Open Redirect cheat sheet handy for the encoding tricks (//attacker.com, \/\/, backslashes, @ userinfo, whitespace) that defeat naive host checks.
Code Injection and Cross-Client Confusion
Authorization code injection is the inverse of interception: the attacker obtains a valid code (their own, or one captured elsewhere) and injects it into a victim's in-flight session by replacing the code in the victim's callback. Without PKCE binding the code to the original browser, the victim's client exchanges the attacker's code, again producing account linkage. This is why "state is present but PKCE is missing" is still exploitable — state stops the cross-session swap only if it is properly bound; PKCE provides the second, cryptographic binding.
Also test mix-up attacks in deployments with multiple authorization servers: if the client doesn't verify which AS issued a code (via the iss parameter or per-AS state), you may be able to get a code issued by a weak/attacker-influenced AS accepted by the client as if it came from the trusted one. After token issuance, validate the resulting ID token too — confused-deputy bugs often pair with weak token verification. Drop the issued JWT into our JWT Decoder to inspect aud, iss, nonce, and signature; for the full token-forgery toolkit see the JWT Attacks guide.
Remediation
Fixing OAuth flows is mostly about enforcing the parts of the spec that exist for exactly these attacks. For defenders and for writing up findings:
- Mandatory, bound
state. Generate a high-entropy random value, store it server-side or in a signed cookie tied to the pre-auth session, and reject any callback whosestatedoesn't match. Treat a missingstateas a hard failure, not a warning. - Enforce PKCE with S256 for every client. Require
code_challengeon/authorizeand a matchingcode_verifieron/token; rejectplainentirely. Apply it to confidential clients too as defense in depth, per OAuth 2.1. - Exact-match
redirect_uri. Compare the full URI string against the registered allowlist — no prefix matching, no wildcard subdomains, no ignoring query/fragment. Normalize before comparison and forbid open redirects anywhere in the OAuth path. - Use
noncefor OIDC and validate it in the ID token to bind the token to the authentication request, complementingstate. - One-time, short-lived codes. Invalidate a code on first use and on any reuse attempt; revoke the associated tokens if reuse is detected.
- Verify the issuer. Honor the
issresponse parameter (RFC 9207) and validateaud/iss/signature on every issued token to defeat mix-up and confused-deputy attacks.
Adopting OAuth 2.1 (which folds in PKCE-by-default, exact redirect matching, and the removal of the implicit flow) closes the bulk of this class. When you report, always pair the proof — a captured cross-session callback, a tokens-issued-without-verifier response, a code delivered to your host — with the specific spec clause being violated; it turns a "best practice" note into an actionable, high-severity finding.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides