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 →
HIGH AI PREVALENCE: LOW auth.jwt.untrusted-verify-key

Untrusted request input flows into the verification key or the algorithms allowlist of jwt.verify(...).

Why AI tools produce this: AI coding tools rarely emit this on their own, but it still slips into assisted edits.

Why this matters

When the attacker controls the key, they sign their own forged token and supply the matching key, so every token "verifies": a complete authentication bypass. When the attacker controls algorithms, they can downgrade verification (e.g. to HS256 against a public key, or to none on older libraries) and defeat the signature check (CWE-347, Improper Verification of Cryptographic Signature).

The verification key and the accepted algorithms must be fixed server-side. Pin algorithms to a constant allowlist ({ algorithms: ['RS256'] }) and resolve the key from trusted configuration or a vetted key set keyed by a validated kid, never from req.query / req.body / req.params / req.headers.

VULNERABLE
vulnerable.ts
import jwt from 'jsonwebtoken';
import type { Request, Response } from 'express';

// Request-controlled verification key (2-argument form): attacker supplies
// both the token and the key it was signed with.
export function verifyWithHeaderKey(req: Request, res: Response): void {
  const token = (req.headers.authorization as string).slice(7);
  // ruleid: auth.jwt.untrusted-verify-key
  const claims = jwt.verify(token, req.headers['x-signing-key'] as string);
  res.json(claims);
}

// Request-controlled key via an intermediate variable (3-argument form).
export function verifyWithBodyKey(req: Request, res: Response): void {
  const key = req.body.publicKey as string;
  // ruleid: auth.jwt.untrusted-verify-key
  const claims = jwt.verify(req.body.token as string, key, { issuer: 'me' });
  res.json(claims);
}

// Request-controlled algorithms allowlist (direct).
export function verifyWithBodyAlgs(req: Request, res: Response): void {
  // ruleid: auth.jwt.untrusted-verify-key
  const claims = jwt.verify(req.body.token as string, process.env.JWT_SECRET as string, {
    algorithms: req.body.algorithms,
  });
  res.json(claims);
}

// Request-controlled algorithm via an intermediate variable.
export function verifyWithHeaderAlg(req: Request, res: Response): void {
  const alg = req.headers['x-alg'] as string;
  // ruleid: auth.jwt.untrusted-verify-key
  const claims = jwt.verify(req.query.token as string, process.env.JWT_SECRET as string, {
    algorithms: [alg],
  });
  res.json(claims);
}
SAFE
safe.ts
import jwt from 'jsonwebtoken';
import type { Request, Response } from 'express';

const ALLOWED_ALGS = ['RS256'];

// Vet a candidate algorithm against a constant allow-list before use.
function validateAlgorithm(candidate: string): string {
  return ALLOWED_ALGS.includes(candidate) ? candidate : 'RS256';
}

// Constant key and constant algorithms — nothing request-controlled reaches
// the verification parameters. The token itself coming from the request is
// expected and must not fire.
export function verify(req: Request, res: Response): void {
  const token = (req.headers.authorization as string).slice(7);
  const claims = jwt.verify(token, process.env.JWT_SECRET as string, {
    algorithms: ['RS256'],
  });
  res.json(claims);
}

// Sanitized: the requested algorithm is checked against the allow-list helper
// before it is passed to verify(), so the taint is cleared.
export function verifyWithVettedAlg(req: Request, res: Response): void {
  const alg = validateAlgorithm(req.body.alg as string);
  const claims = jwt.verify(req.body.token as string, process.env.JWT_SECRET as string, {
    algorithms: [alg],
  });
  res.json(claims);
}

const KEYS: Record<string, string> = {
  default: process.env.JWT_PUBLIC_KEY as string,
};

// The key is resolved from a trusted key set, never from the request body.
export function verifyWithResolvedKey(req: Request, res: Response): void {
  const key = KEYS.default;
  const claims = jwt.verify(req.body.token as string, key, { algorithms: ['RS256'] });
  res.json(claims);
}

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.jwt.untrusted-verify-key -- <reason>

References

https://cwe.mitre.org/data/definitions/347.html ↗https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 ↗https://owasp.org/www-project-api-security/ ↗