Timing Attacks and Constant-Time Comparisons
AI generated
OWASP
0x00
OWASP Cryptography · Timing Side Channels
Timing Attacks and Constant-Time Comparisons
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.

12 min read Timing Attacks hash_equals()

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.

11. FAQ: Timing Attacks

1What is a timing attack?
A timing attack exploits measurable differences in how long a comparison takes to infer something about a secret value, even though the visible result of the comparison looks identical either way.
2Why is the === operator in PHP timing sensitive?
Because the underlying byte-by-byte comparison stops at the first mismatching byte, which makes the execution time depend slightly on how many leading bytes matched.
3What does hash_equals do differently?
hash_equals always compares every byte of both values and combines the results bitwise, so execution time depends only on length, never on the content of the values.
4Is every string comparison in an application a risk?
No, only comparisons between a secret value and input an attacker can vary repeatedly are relevant. Comparisons between two public values are not a concern.
5Do password comparisons need manual hardening with hash_equals?
Usually not, since password_verify already performs a constant-time comparison internally. Only custom checks that bypass password_verify explicitly need hash_equals.
6Are timing attacks practically feasible over the open internet?
Network jitter makes individual measurements much harder, but with statistical methods and enough measurements, such attacks have already been demonstrated successfully against real applications.
7Are there similar functions in other languages?
Yes, such as hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js. The principle of constant-time comparison is consistent across languages.
8How do you find at-risk spots in your own code?
By specifically searching for === comparisons where at least one side is a secret, security relevant value such as an API key or a signature.
9Does rate limiting help against timing attacks?
Rate limiting significantly reduces the number of measurements available to an attacker and is a useful addition, but it does not replace the constant-time comparison function itself.
10Must hash_equals be used for every HMAC comparison?
Yes, anywhere a server-computed HMAC signature is checked against a signature submitted by the client, hash_equals should consistently replace a plain comparison.