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.flow.timing-unsafe-compare

A secret-shaped value (password, token, secret, apiKey, csrf, hmac) is being compared with === / !== / string1 == string2.

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

Why this matters

JavaScript's equality operators short-circuit on the first differing byte, which leaks the matching prefix length over the wire, a classic timing-attack vector.

Use crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) (both buffers must be the same length, so hash first if needed). For password verification, use argon2.verify, bcrypt.compare, or scrypt. They handle constant-time comparison for you.

VULNERABLE
vulnerable.ts
declare const hashedPassword: string;
declare const submittedToken: string;
declare const expectedHmac: string;
declare const apiKey: string;

export function loginBad(input: string) {
  // ruleid: auth.flow.timing-unsafe-compare
  return input === hashedPassword;
}

export function checkApiKey(provided: string) {
  // ruleid: auth.flow.timing-unsafe-compare
  return provided === apiKey;
}

export function verifyHmac(actualHmac: string) {
  // ruleid: auth.flow.timing-unsafe-compare
  return expectedHmac !== actualHmac;
}

export function loginLoose(input: string) {
  // ruleid: auth.flow.timing-unsafe-compare -- loose equality is just as unsafe
  return input == hashedPassword;
}
SAFE
safe.ts
import { timingSafeEqual } from 'node:crypto';
import argon2 from 'argon2';

// ok: auth.flow.timing-unsafe-compare
export function loginGood(input: string, stored: string) {
  return argon2.verify(stored, input);
}

// ok: auth.flow.timing-unsafe-compare
export function verifyHmacSafe(expected: Buffer, actual: Buffer) {
  return expected.length === actual.length && timingSafeEqual(expected, actual);
}

// ok: auth.flow.timing-unsafe-compare -- comparing non-secret values is fine
export function compareUsernames(a: string, b: string) {
  return a === b;
}

// ok: auth.flow.timing-unsafe-compare -- loose null/presence checks are not comparisons
export function presenceChecks(token?: string) {
  if (token == null) return false;
  if (typeof token != 'string') return false;
  return token.length != 0;
}

// ok: auth.flow.timing-unsafe-compare -- comparing a secret-named value to a
// string literal is not a timing target (the literal is public in source).
export function demoPassword(password: string) {
  return password !== 'password';
}

// ok: auth.flow.timing-unsafe-compare -- boolean-literal / feature-flag checks
export function featureFlag(opts: { idToken?: boolean }) {
  if (opts.idToken === false) return false;
  return opts.idToken !== true;
}

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.flow.timing-unsafe-compare -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html ↗https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b ↗