Skip to main content
interlace
Plugin: node-securityRules

no-timing-unsafe-compare

Disallow timing-unsafe comparison of secrets

Detects timing-unsafe comparison of secrets using === or == operators. This rule is part of eslint-plugin-node-security and provides LLM-optimized error messages with fix suggestions.

🚨 Security rule | 💡 Provides suggestions | ⚠️ Set to error in recommended

Quick Summary

AspectDetails
CWE ReferenceCWE-208 (Timing Attack)
SeverityHigh (security vulnerability)
Auto-Fix💡 Suggests crypto.timingSafeEqual()
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForNode.js applications comparing tokens, secrets, or signatures

Vulnerability and Risk

Vulnerability: Using === to compare secrets enables timing attacks. The comparison short-circuits on the first mismatched character, so the time taken reveals information about how many characters matched.

Risk: An attacker can measure comparison times to guess secret values character-by-character. For example, comparing API keys or HMAC signatures with === allows attackers to brute-force the correct value.

Rule Details

This rule detects ===, ==, !==, and != comparisons where at least one operand has a name suggesting it's a secret (token, password, key, secret, hash, signature, etc.).

Why This Matters

RiskImpactSolution
⏱️ Timing LeakComparison time reveals match lengthUse crypto.timingSafeEqual()
🔑 Secret Brute ForceAttack one character at a timeConstant-time comparison
🔒 Token BypassForge tokens by timing analysisNever use === for secrets

Configuration

OptionTypeDefaultDescription
secretPatternsstring[]['token', 'secret', 'key', ...]Variable name patterns for secrets
{
  rules: {
    'node-security/no-timing-unsafe-compare': ['error', {
      secretPatterns: [
        'token', 'secret', 'key', 'password', 'hash', 'signature',
        'mac', 'hmac', 'digest', 'apiKey', 'api_key', 'auth',
        'credential', 'bearer', 'jwt', 'csrf', 'nonce'
      ]
    }]
  }
}

Examples

❌ Incorrect

// Timing-unsafe comparison of API key - HIGH risk
function validateApiKey(userKey: string, storedKey: string) {
  return userKey === storedKey; // ❌ Leaks timing information
}

// Comparing tokens with ===
if (submittedToken === validToken) {
  // ❌ Vulnerable
  grantAccess();
}

// HMAC verification with ===
const expectedHash = crypto.createHmac('sha256', secret).update(data).digest();
if (receivedHash === expectedHash) {
  // ❌ Timing attack possible
  processData();
}

✅ Correct

import crypto from 'crypto';

// Constant-time comparison
function validateApiKey(userKey: string, storedKey: string) {
  const userBuffer = Buffer.from(userKey);
  const storedBuffer = Buffer.from(storedKey);

  // Lengths must match for timingSafeEqual
  if (userBuffer.length !== storedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(userBuffer, storedBuffer); // ✅ Safe
}

// HMAC verification (constant-time)
function verifyHmac(data: Buffer, receivedHmac: Buffer, secret: Buffer) {
  const expectedHmac = crypto
    .createHmac('sha256', secret)
    .update(data)
    .digest();

  if (receivedHmac.length !== expectedHmac.length) {
    return false;
  }

  return crypto.timingSafeEqual(receivedHmac, expectedHmac); // ✅ Safe
}

// JWT verification (use library)
import jwt from 'jsonwebtoken';
jwt.verify(token, secret); // ✅ Library handles timing-safe comparison

How Timing Attacks Work

Secret:   "s3cr3t_k3y"
Guess 1:  "aaaaaaaaaa" → Fast rejection (0 chars match)
Guess 2:  "saaaaaaaaa" → Slightly slower (1 char matches)
Guess 3:  "s3aaaaaaaa" → Even slower (2 chars match)
...
Guess N:  "s3cr3t_k3y" → Slowest (all chars match)

Each additional matching character adds measurable time, allowing attackers to discover secrets character-by-character.

Security Impact

VulnerabilityCWEOWASPCVSSImpact
Timing Discrepancy208A02:20215.9 MediumSecret value leak
Observable Timing208A02:20215.3 MediumBrute force enablement

Migration Guide

Phase 1: Discovery

{
  rules: {
    'node-security/no-timing-unsafe-compare': 'warn'
  }
}

Phase 2: Replacement

// Replace === with timingSafeEqual
if (userToken === validToken)  // ❌ Before
if (crypto.timingSafeEqual(Buffer.from(userToken), Buffer.from(validToken)))  // ✅ After

Phase 3: Enforcement

{
  rules: {
    'node-security/no-timing-unsafe-compare': 'error'
  }
}

Known False Negatives

Non-Standard Variable Names

Why: Only configured patterns are detected.

// ❌ NOT DETECTED - unusual variable name
if (userValue === dbValue) { ... }  // Actually comparing tokens

Mitigation: Add patterns to configuration or use consistent naming.

Indirect Comparisons

Why: Cross-function data flow not tracked.

// ❌ NOT DETECTED
function compare(a, b) {
  return a === b;
}
compare(userToken, validToken);

Mitigation: Search codebase for comparison patterns.

Further Reading

⚙️ Options

OptionTypeDefaultDescription
secretPatternsstring[]["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"]Variable name patterns that indicate secrets
untrustedSourcesstring[]["req","request","ctx","event"]Identifier roots treated as attacker-controlled (default: req, request, ctx, event)
reportUnverifiedComparisonsbooleanfalseReport on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour.
nonSecretWordsstring[]["author","authors","authored","authoring","authorship","hashtag","hashtags"]Whole words that mean a secretPatterns match was a collision (default: author, authors, authored, authoring, authorship, hashtag, hashtags). Replaces the list.
nonSecretTailsstring[]["count","counts","limit","limits","usage","total","size","length","price","cost","quota","address","addresses","index","rank","percent","path","paths","pathname","pathnames","endpoint","endpoints","route","routes","hostname","host","port","origin","kind","kinds","type","types","flag","flags","category"]Trailing words that make the value a measurement or location rather than a credential (default: count, counts, limit, limits, usage, total, size, length, price, cost, quota, address, addresses, index, rank, percent, path, paths, pathname, pathnames, endpoint, endpoints, route, routes, hostname, host, port, origin, kind, kinds, type, types, flag, flags, category). Replaces the list.

Both halves of the vocabulary are yours

secretPatterns says this is a secret; nonSecretWords and nonSecretTails say …except when. All three are substring-or-word matches on identifiers, so all three are tunable — a rule that lets you widen the positive list but not the exclusions makes the widening unusable.

{
  "node-security/no-timing-unsafe-compare": ["error", {
    // Re-admit a bare `key`, then exclude the senses this codebase uses.
    "secretPatterns": ["token", "secret", "key", "signature"],
    "nonSecretWords": ["author", "hashtag", "keyboard", "keyring"],
    // `tokenChapter` is a position in a document, not a credential.
    "nonSecretTails": ["count", "limit", "size", "length", "index", "chapter"]
  }]
}

nonSecretWords matches any word of the identifier; nonSecretTails matches only the last one. Setting either to [] removes that guard, which restores the collisions it exists to prevent (authorId matching auth, tokenCount matching token).

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.