← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

hash_equals()

php PHP 5.6+ Intermediate
debt(d5/e3/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

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.

e3 Effort Remediation debt — work required to fix once spotted

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.

b3 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

hash_equals() constant time comparison timing safe compare

TL;DR

Constant-time string comparison that prevents timing attacks when validating tokens and CSRF values.

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

Using === to compare hashes is safe enough. === short-circuits on the first differing byte, leaking timing information. hash_equals() always compares the full string in constant time regardless of where the first difference occurs.

Why It Matters

hash_equals() compares two strings in constant time — preventing timing attacks where an attacker learns how many characters of a guess match by measuring response time differences.

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

✗ Vulnerable
// 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();
✓ Fixed
// 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))) { ... }

Added 15 Mar 2026
Edited 22 Mar 2026
Views 36
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 1 ping M 0 pings T 1 ping W 0 pings T 1 ping F 0 pings S 0 pings S 2 pings M 0 pings T 0 pings W 1 ping T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S
No pings yet today
Perplexity 10 Amazonbot 6 Ahrefs 4 Unknown AI 3 Google 2 SEMrush 2 ChatGPT 2 Majestic 1
crawler 27 crawler_json 2 pre-tracking 1
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Replace every security-sensitive string comparison (token, HMAC, password reset) with hash_equals($known, $user_supplied) — it always takes the same time regardless of where they differ
📦 Applies To
PHP 5.6+ web api cli
🔗 Prerequisites
🔍 Detection Hints
=== or == comparing CSRF tokens HMAC signatures API keys password reset tokens
Auto-detectable: ✓ Yes semgrep psalm
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Low ✓ Auto-fixable Fix: Low Context: Line
CWE-208 CWE-697

✓ schema.org compliant