hash_equals()
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep and psalm — both specialist/SAST tools. The pattern (=== or == on security-sensitive strings) is not caught by default PHP linters or the compiler; it requires a configured semgrep rule or psalm security plugin to flag it. It won't surface in production logs or errors — it silently leaks timing information.
Closest to 'simple parameterised fix' (e3). The quick_fix is a direct replacement: swap === or == with hash_equals($known, $user_supplied). However, the common_mistakes note that the fix must be applied consistently across all token/HMAC comparisons in the codebase (not just one place), which makes it slightly more than a single-line patch but still a localised find-and-replace refactor rather than a multi-file architectural change.
Closest to 'localised tax' (b3). The choice applies broadly across web, api, and cli contexts wherever security-sensitive string comparisons occur, but it does not reshape the entire architecture. Each comparison site is an independent fix; the burden is a recurring but contained code-review and implementation tax rather than a deep structural constraint on the system.
Closest to 'serious trap' (t7). The misconception field documents exactly this: developers confidently believe === is safe for hash comparison because it works for equality in all other PHP contexts. The short-circuiting behaviour that causes the timing leak is an invisible implementation detail — === behaves 'correctly' in terms of result but leaks timing information in a way that contradicts how developers expect it to behave relative to other equality checks. This contradicts the mental model formed from normal string equality usage.
Also Known As
TL;DR
Explanation
Regular string comparison (=== or strcmp) returns early as soon as a character mismatch is found. An attacker can measure response time differences to determine how many characters of a token they have correct — a timing attack. hash_equals($known, $user) always takes the same amount of time regardless of where the strings differ, making timing attacks infeasible. Always use it when comparing security tokens, CSRF values, HMAC signatures, or API keys.
Common Misconception
Why It Matters
Common Mistakes
- Using === or == to compare HMACs, tokens, or password hashes — both have early exit and leak timing information.
- Using strcmp() as an alternative — it is also not constant time and has null byte truncation issues.
- Applying hash_equals() only to HMAC comparison but not to token comparison elsewhere in the application.
- Passing null or non-string values — hash_equals() requires two strings of the same type.
Code Examples
// Timing-vulnerable token comparison:
if ($token === $expected) { /* early exit leaks information */ }
if (strcmp($token, $expected) === 0) { /* same problem */ }
// Safe:
if (!hash_equals($expected, $token)) throw new InvalidTokenException();
// Constant-time comparison — prevents timing attacks
if (hash_equals(\$expectedToken, \$userToken)) { /* authenticated */ }
// Verify HMAC signatures:
\$expected = hash_hmac('sha256', \$requestBody, \$secret);
\$provided = \$_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!hash_equals(\$expected, \$provided)) abort(401);
// Both arguments must be same length — safe with hashes (always equal length).
// For raw tokens, hash first:
if (hash_equals(hash('sha256', \$stored), hash('sha256', \$input))) { ... }