Skip to content

auth.flow.timing-unsafe-compare

A secret-shaped value (password, token, secret, apiKey,

OAuthLint idAUTH-FLOW-004
SeverityWARNING
LLM prevalenceMEDIUM
CWECWE-208
OWASPAPI2:2023
Languagesjavascript, typescript

Why this matters

A secret-shaped value (password, token, secret, apiKey, csrf, hmac) is being compared with === / !== / string1 == string2. 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

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

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 (when you really must)

ts
// oauthlint-disable-next-line auth.flow.timing-unsafe-compare -- <reason>
thisLineWouldOtherwiseTriggerTheRule();

Disable directives are line-scoped by design — wholesale silencing of a rule across the codebase is intentionally not supported, because the next reviewer needs to see exactly which lines opted out.

References

Released under the MIT License · powered by Semgrep