When execution time itself gives away a secret
Different execution times in string comparisons can let attackers guess secret values, such as API keys or HMAC signatures, byte by byte. We explain the underlying mechanism, show hash_equals() as the fix in PHP, and cover which comparisons are actually timing sensitive.
Table of Contents
- 1. What Are Timing Attacks?
- 2. Where the Problem Shows Up in Practice
- 3. Why hash_equals() Solves the Problem
- 4. Which Comparisons Are Actually Timing Sensitive
- 5. The Special Case of Password Hashes
- 6. Constant-Time Comparisons in Other Contexts
- 7. How Practically Exploitable Are Timing Attacks?
- 8. Common Mistakes with Security Critical Comparisons
- 9. Best Practices and Checklist
- 10. Summary
- 11. FAQ
1. What Are Timing Attacks?
A timing attack exploits the fact that a program's execution time can depend on the values being processed, even though the outcome of the operation looks identical from the outside. With a naive string comparison, the kind PHP's === operator performs internally, the comparison happens byte by byte and stops immediately at the first mismatching byte.
An attacker who sends many requests with different guesses and measures the response time can infer how many leading bytes were already correct. The more bytes match at the start, the slightly longer the comparison takes, because it aborts later. Given enough measurements, a secret value can be reconstructed byte by byte this way.
2. Where the Problem Shows Up in Practice
Timing sensitive comparisons mostly involve a server-known secret being compared against a value submitted by the client: API keys in header fields, HMAC signatures during webhook validation, password reset tokens, or CSRF tokens. Anywhere a plain === comparison sits between a secret value and an attacker-controlled value, a timing risk exists in theory.
The vulnerable method in the example below uses the standard comparison operator, whose execution time can theoretically correlate with the number of matching leading bytes. The fixed method uses hash_equals instead, which always compares every byte of both values internally, regardless of where a difference occurs, guaranteeing constant execution time regardless of content.
<?php
declare(strict_types=1);
namespace App\Security;
final class WebhookSignatureValidator
{
public function __construct(
private readonly string $webhookSecret,
) {
}
// Vulnerable: byte-by-byte comparison stops at the first difference
public function isValidVulnerable(string $payload, string $providedSignature): bool
{
$expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret);
return $expectedSignature === $providedSignature;
}
// Fixed: constant execution time regardless of content
public function isValidSecured(string $payload, string $providedSignature): bool
{
$expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret);
return hash_equals($expectedSignature, $providedSignature);
}
}
3. Why hash_equals() Solves the Problem
hash_equals was built specifically for security critical comparisons and has been part of the standard library since PHP 5.6. The function compares two strings so that the time required depends only on the length of the strings, never on where a difference occurs. Internally, every byte gets compared and the results are combined through a bitwise operation instead of stopping at the first mismatch.
It matters that hash_equals is meant exclusively for comparing two already known, fixed values, such as an expected and a received signature. For general string comparisons without a security critical context, the regular === operator remains the correct and faster choice.
4. Which Comparisons Are Actually Timing Sensitive
Not every string comparison in an application is a meaningful security risk. Only comparisons where a secret value is checked against input that an attacker can submit repeatedly with variation, and where the response time is measurable by the attacker, for example through repeated API calls, are critical.
A comparison between two public values, such as two product names in a sorting function, is not timing sensitive, since no secret is involved. Comparisons where an attacker cannot make repeated, deliberately varied requests in the first place, for example under strict rate limiting that locks out after a few failed attempts, are equally irrelevant.
5. The Special Case of Password Hashes
Password comparisons in practice are almost never done directly, but through password_verify, which already performs a constant-time comparison of the hash value internally. It still matters that password_verify is only responsible for comparing hash values, the hashing itself should happen with password_hash and a suitable algorithm such as Argon2id.
A common mistake is comparing a manually computed hash result against the stored hash with a plain === operator instead of hash_equals, for example in a custom check that does not go through password_verify. In that case too, hash_equals is the correct choice.
6. Constant-Time Comparisons in Other Contexts
The principle of constant-time comparison is not limited to PHP. Almost every modern language offers a comparable function, such as hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js. Libraries for JWT validation, OAuth signatures, or webhook verification usually already use these functions internally by default, so developers rarely need to think about this at all as long as they rely on an established library instead of a hand-rolled implementation.
Problems tend to arise when developers build a supposedly simpler custom check around such a library, for example an extra manual comparison before the actual library call, and accidentally reintroduce a non-constant-time comparison in the process. A code review should therefore check not only that a secure library is in use, but also that nothing insecure has been layered around it.
7. How Practically Exploitable Are Timing Attacks?
In local networks with very low and stable latency, timing attacks are well documented and practically feasible. Over the open internet, network jitter, load balancers, and variable server load make measurement significantly harder, which is why a single measurement is barely meaningful on its own. Attackers can partially work around this by running many parallel measurements and filtering out statistical outliers, which lowers the practical effort involved without eliminating it entirely.
With statistical methods and a very large number of measurements per byte, though, these disturbances can largely be averaged out, as shown by several academic papers and practical proof-of-concept attacks against real web applications. Hardening with hash_equals is therefore not a theoretical precaution but a genuinely effective mitigation that is cheap to implement.
8. Common Mistakes with Security Critical Comparisons
A common mistake is using hash_equals in only one place in the codebase, such as the primary signature check, while another location, for example an additional internal debug or test mode, still uses a plain === comparison for the same secret value.
Another mistake is mixing up the argument order for hash_equals, or taking one of the two values straight from unvalidated user input without checking its length or type, which in rare cases can itself introduce a side effect.
9. Best Practices and Checklist
Every comparison between a server-known secret value and client-submitted input should consistently use hash_equals instead of the standard comparison operator, especially for API keys, HMAC signatures, and token checks outside of password_verify.
It also helps to have a code review standard that specifically looks for === comparisons involving secret values, along with general rate limiting on security critical endpoints, which further limits the number of measurements available to an attacker on top of the constant-time check itself.
| Comparison Type | Example | Timing Risk | Recommended Fix |
|---|---|---|---|
| API key check | X-Api-Key header against stored value | High with direct === | hash_equals() |
| HMAC signature | Webhook payload signature | High with direct === | hash_equals() |
| Password hash | Login check | Already constant | password_verify() |
| Public value | Sorting product names | No risk | Regular === comparison is fine |
Mironsoft
Security audits, OWASP-compliant hardening, and secure architecture
Applications that actually hold up against a real attack attempt?
We review existing applications for classic OWASP vulnerabilities, insecure authentication, and missing input validation, then build an architecture that structurally reduces attack surface instead of just patching individual symptoms.
Security Audit
Systematically checking OWASP Top 10, auth flows, and input validation for vulnerabilities.
Secure Architecture
Building rate limiting, encryption, and access controls correctly from the ground up.
Incident Readiness
Establishing logging, monitoring, and response processes for when things go wrong.
10. Summary
Timing Attacks
Root Cause
Byte-by-byte comparison stops at the first mismatch.
Detection
Statistical timing measurement of repeated, varied requests.
Fix
hash_equals() for every security critical comparison.
Prevention
Code review for === on secrets, rate limiting.