← Toutes les actualités
security4 min de lecture

Missing OAuth state and PKCE: the CSRF and code interception risks vibe coders ship

Par Seaworthy · 28 juin 2026

Red padlock resting on a black computer keyboard

Photo by FlyD on Unsplash

What LLMs get wrong about OAuth

When you ask an AI coding assistant to "add Google login", you get working code fast. The happy path works: user clicks, tokens arrive.

But fast is not the same as secure. Two of the most common omissions in AI-generated OAuth flows are the state parameter and PKCE (Proof Key for Code Exchange). Both have been best practice for years. Both are routinely dropped.

The state parameter and CSRF

The state parameter is how you tie an authorization request to the browser session that started it. Without it, your callback endpoint will accept any authorization code that arrives, regardless of whether your app initiated the flow.

Here is a minimal Node.js example of what gets generated all the time:

// Vulnerable: no state parameter
app.get('/auth/google', (req, res) => {
  const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  url.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);
  url.searchParams.set('redirect_uri', 'https://myapp.com/auth/callback');
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('scope', 'openid email profile');
  res.redirect(url.toString());
});

app.get('/auth/callback', async (req, res) => {
  const { code } = req.query;
  // Exchanges any code, no questions asked
  const tokens = await exchangeCode(code);
  req.session.user = await fetchUser(tokens.access_token);
  res.redirect('/dashboard');
});

This is textbook CSRF. An attacker crafts a malicious link that triggers the callback with their own authorization code. Your server exchanges it, logs the attacker's account into the victim's session, and the victim is now authenticated as the attacker. The exact outcome depends on the app: account takeover, data injection under the victim's identity, or silent privilege changes.

The fix is two extra lines on the way out and one check on the way back:

import crypto from 'node:crypto';

// Safer: generate and verify state
app.get('/auth/google', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;

  const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  url.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);
  url.searchParams.set('redirect_uri', 'https://myapp.com/auth/callback');
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('scope', 'openid email profile');
  url.searchParams.set('state', state);
  res.redirect(url.toString());
});

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;

  if (!state || state !== req.session.oauthState) {
    return res.status(403).send('State mismatch: possible CSRF attack');
  }
  delete req.session.oauthState;

  const tokens = await exchangeCode(code);
  req.session.user = await fetchUser(tokens.access_token);
  res.redirect('/dashboard');
});

Not complex. No exotic dependencies: just a random nonce stored in the server-side session, compared on return.

PKCE: the forgotten layer for public clients

PKCE (pronounced "pixy") was designed for native apps and SPAs where you cannot safely store a client secret. The protocol works like this: generate a random code_verifier before the authorization request, derive a code_challenge from it (SHA-256 hash, base64url-encoded), send the challenge with the auth request, then send the verifier when exchanging the code. The authorization server checks they match.

Without PKCE on a public client, an attacker who intercepts the authorization code through a malicious redirect URI or a compromised browser extension can exchange it themselves. The authorization server has no way to verify the legitimate app initiated the flow.

AI-generated code for SPAs almost always skips this step. The PKCE setup looks like boilerplate; the assistant drops it to keep the snippet readable. That is the wrong trade-off.

For a SPA or mobile app, generate the verifier and challenge before the redirect:

function base64urlEncode(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

async function buildAuthUrl() {
  const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
  sessionStorage.setItem('pkce_verifier', verifier);

  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const challenge = base64urlEncode(hash);

  const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  url.searchParams.set('client_id', CLIENT_ID);
  url.searchParams.set('redirect_uri', REDIRECT_URI);
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('scope', 'openid email profile');
  url.searchParams.set('code_challenge', challenge);
  url.searchParams.set('code_challenge_method', 'S256');
  return url.toString();
}

Then pass the code_verifier from sessionStorage when you exchange the code. The authorization server rejects any exchange where the verifier does not match the original challenge. An intercepted code is dead without it.

Why AI-generated code gets this wrong

LLMs optimize for "code that runs". OAuth flows that skip state and PKCE still run. The tokens arrive. The CSRF vector and the code interception window are invisible until something goes wrong.

There is also a training data problem. Older OAuth tutorials and Stack Overflow answers written before 2019 do not include PKCE because RFC 7636 was not yet published. That material is baked into what every LLM learned from, and the omission propagates forward into every auth flow a vibe coder ships today.

What to check before you ship

If you are using an OAuth 2.0 authorization code flow, ask yourself:

  • Is a cryptographically random state value generated per request, stored server-side, and verified on callback?
  • If this is a SPA or native app: is PKCE configured with code_challenge_method=S256?
  • Did you actually check these things, or are you trusting the AI that generated the snippet?

If the answer to the last point is "I'm trusting the AI", your auth flow deserves a second look. Read your library's documentation rather than shipping on faith.

Seaworthy's static analysis checks OAuth flows for missing state parameters and absent PKCE configuration across your codebase, so these omissions surface at scan time rather than in a post-incident review.

Cet article a été généré par IA et résume des sources publiquement disponibles.

Par les créateurs de Seaworthy

Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.

Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.

$ npx seaworthycode