The OAuth state parameter is the CSRF token for the OAuth flow. Without it, an attacker can initiate an OAuth authorization on behalf of a victim and then complete the flow with the attacker's authorization code, linking the victim's account to the attacker's identity (account linking attack). CWE-352 (CSRF) and CWE-601 (Open Redirect) are both implicated in OAuth flow manipulation; CAPEC-62 documents the OAuth CSRF attack specifically. OWASP A01 (Broken Access Control) covers this when an attacker can substitute their own authorization code. State validation is a mandatory requirement of RFC 6749.
High because OAuth CSRF without state validation can silently link a victim's account to an attacker's social identity or trigger unauthorized account access during the callback.
Generate a per-flow cryptographically random state, store it in an HttpOnly cookie before redirecting, and verify it matches on callback:
// Initiation — generate and store state
const state = crypto.randomBytes(16).toString('hex')
response.cookies.set('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'lax' })
return redirect(provider.buildAuthUrl({ state, redirect_uri: callbackUrl }))
// Callback — verify state before processing code
const storedState = request.cookies.get('oauth_state')?.value
const returnedState = searchParams.get('state')
if (!storedState || storedState !== returnedState) {
return Response.json({ error: 'State mismatch — possible CSRF' }, { status: 400 })
}
NextAuth and Supabase Auth handle state generation and verification automatically. If you've written a custom OAuth callback handler, do not skip this step.
ID: saas-authentication.social-auth.oauth-state-validation
Severity: high
What to look for: Examine the OAuth implementation. The OAuth 2.0 state parameter prevents CSRF during the OAuth flow. Check: (1) is a random state value generated before redirecting to the OAuth provider? (2) is it stored in the session before the redirect? (3) is the state returned in the callback URL compared against the stored state? Look in the OAuth callback handler (/api/auth/callback/[provider], or equivalent). Managed providers (NextAuth, Clerk, Supabase Auth) handle this by default — verify no override disables it. Count all instances found and enumerate each.
Pass criteria: A cryptographically random state value is generated per OAuth initiation, stored server-side, and verified in the callback. Mismatch between expected and returned state aborts the flow with an error. Libraries that implement this by default count as passing. At least 1 implementation must be confirmed.
Fail criteria: Custom OAuth implementation does not use the state parameter, or does not verify the returned state against the stored value. State is predictable (not cryptographically random).
Skip (N/A) when: No OAuth or social auth integrations. Signal: no OAuth provider dependencies, no social login buttons.
Cross-reference: The social-callback-whitelist check verifies the redirect URL restrictions that complement state parameter validation.
Detail on fail: "Custom OAuth implementation at /api/auth/oauth/google does not generate or validate the state parameter — vulnerable to OAuth CSRF attacks".
Remediation: Without state validation, an attacker can construct a callback URL that logs a victim into the attacker's social account (account linking attack), or CSRF the OAuth flow:
// OAuth initiation: generate and store state
const state = crypto.randomBytes(16).toString('hex')
request.cookies.set('oauth_state', state, { httpOnly: true, secure: true })
const authUrl = provider.buildAuthUrl({ state, redirect_uri: callbackUrl })
return redirect(authUrl)
// OAuth callback: verify state
const storedState = request.cookies.get('oauth_state')?.value
const returnedState = searchParams.get('state')
if (!storedState || storedState !== returnedState) {
return Response.json({ error: 'State mismatch' }, { status: 400 })
}
If using NextAuth, state handling is built in. Do not use custom OAuth flows unless you understand the full security requirements.