OAuth state parameter validated on callback
Why it matters
OAuth without state parameter validation (CWE-352, CWE-601) is a CSRF attack on the OAuth flow itself. An attacker who tricks a victim into visiting a crafted URL can complete an OAuth authorization as the attacker, binding their identity to the victim's account — a classic account takeover vector. OWASP A01 (Broken Access Control) and NIST 800-53 IA-8 both require integrity checks on authentication callbacks. The state parameter is the mechanism: it must be a cryptographically random value generated by the application and stored server-side (not just echoed back) so the callback can verify the request originated from the expected user.
Severity rationale
Medium because exploitation requires luring the victim to a crafted URL, but a successful attack results in account takeover by linking an attacker-controlled OAuth identity.
Remediation
Generate a random state, store it in an httpOnly cookie, and validate it in the callback before exchanging the authorization code:
// Before redirecting to provider:
const state = crypto.randomUUID()
cookies().set('oauth_state', state, { httpOnly: true, secure: true, maxAge: 600 })
redirect(`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&state=${state}`)
// In the callback route:
const returnedState = searchParams.get('state')
const storedState = cookies().get('oauth_state')?.value
if (!returnedState || returnedState !== storedState) {
return Response.json({ error: 'Invalid state' }, { status: 400 })
}
cookies().delete('oauth_state')
// Proceed with code exchange
Do not compare state to a static value — it must be unique per authorization request and session-bound.
Detection
-
ID:
oauth-state-validation -
Severity:
medium -
What to look for: Enumerate every OAuth callback route. For each callback, examine OAuth flow implementation. Check the authorization redirect to see if a
stateparameter is generated and included. Then check the callback route to verify the returnedstatematches the expected value (stored in session or cookie). This prevents CSRF attacks on OAuth flows. -
Pass criteria: OAuth authorization URLs include a cryptographically random
stateparameter stored in the session. The callback handler verifies the returnedstatematches before exchanging the authorization code — 100% of callbacks must validate state parameter before code exchange. Report: "X OAuth callback routes found, all Y validate the state parameter." -
Fail criteria: No
stateparameter in OAuth flows. Orstateis present but not validated on callback. -
Skip (N/A) when: The application has no OAuth implementation (no third-party login buttons). Managed auth providers (NextAuth.js, Clerk) handle this automatically — verify their implementation handles state internally.
-
Detail on fail: Quote the actual callback code showing the missing state check. Example:
"GitHub OAuth callback route does not validate state parameter — CSRF attack on OAuth possible"or"OAuth state generated but compared to a static value rather than session-stored random value" -
Remediation: Implement state validation:
// When redirecting to OAuth provider: const state = crypto.randomUUID() // Store in session (server-side) or encrypted cookie cookies().set('oauth_state', state, { httpOnly: true, secure: true, maxAge: 600 }) const authUrl = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&state=${state}&scope=user:email` redirect(authUrl) // In the callback handler: export async function GET(req: Request) { const { searchParams } = new URL(req.url) const code = searchParams.get('code') const returnedState = searchParams.get('state') const storedState = cookies().get('oauth_state')?.value if (!returnedState || !storedState || returnedState !== storedState) { return Response.json({ error: 'Invalid state parameter' }, { status: 400 }) } // Clear state cookie, exchange code for token cookies().delete('oauth_state') // ... token exchange }
External references
- cwe · CWE-352 — Cross-Site Request Forgery (CSRF)
- cwe · CWE-601 — URL Redirection to Untrusted Site
- owasp:2021 · A01 — Broken Access Control
- nist:rev5 · IA-8 — Identification and Authentication (Non-Organizational Users)
Taxons
History
- 2026-04-18·v1.0.0·Initial import from security-hardening·automated