v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
MEDIUM AI PREVALENCE: MEDIUM auth.oauth.static-state

OAuth authorization request sends a hardcoded, constant state value.

Why AI tools produce this: AI coding tools produce this regularly, typically when prompted for a shortcut or a quick fix.

Why this matters

A static state provides ZERO CSRF protection: the whole point is an unguessable, per-request value that you store and then compare on the callback. A literal that ships in your source is known to everyone and identical on every request, so an attacker can forge a matching callback.

Generate state fresh per request from a CSPRNG (crypto.randomBytes(32).toString('hex') / crypto.getRandomValues), persist it in the session/cookie, and verify it when the provider redirects back.

VULNERABLE
vulnerable.ts
// Hardcoded, constant `state` — identical on every request, so it provides no
// CSRF protection at all.

// ruleid: auth.oauth.static-state
export const authorizeUrl =
  'https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&state=xyz123&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb';

export function buildAuthorize(res: { redirect: (url: string) => void }) {
  // ruleid: auth.oauth.static-state
  const params = new URLSearchParams({
    client_id: 'abc',
    response_type: 'code',
    redirect_uri: 'https://app.example.com/cb',
    scope: 'openid email',
    state: 'static-state-value',
  });
  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`);
}
SAFE
safe.ts
import { randomBytes } from 'node:crypto';

// Per-request `state` from a CSPRNG, stored for callback verification.
export function buildAuthorize(
  res: { redirect: (url: string) => void },
  store: (s: string) => void,
) {
  const state = randomBytes(32).toString('hex');
  store(state);
  // ok: auth.oauth.static-state
  const params = new URLSearchParams({
    client_id: 'abc',
    response_type: 'code',
    redirect_uri: 'https://app.example.com/cb',
    scope: 'openid email',
    state,
  });
  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`);
}

// Inline URL whose state is interpolated per request (template literal) — not a
// hardcoded constant.
export function inlineDynamic(state: string): string {
  // ok: auth.oauth.static-state
  return `https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&state=${state}`;
}

// Per-literal proof / negative control: `response_type=` lives in ONE string
// literal and a constant `state=` in a DIFFERENT one. The old two-regex form
// combined them across literals into a false positive; the single per-literal
// regex must not flag either of these.
// ok: auth.oauth.static-state
export const authEndpoint =
  'https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code';
// ok: auth.oauth.static-state
export const someStateString = 'https://example.com/cb?state=teapot';

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.oauth.static-state -- <reason>

References

https://datatracker.ietf.org/doc/html/rfc6749#section-10.12 ↗https://cwe.mitre.org/data/definitions/330.html ↗