- How string comparison works in PHP
- What is a timing attack?
- The real issue is timing, not == vs ===
- What does hash_equals() do differently?
- Incorrect comparison example (do not use)
- Correct comparison example (recommended)
- hash_equals() is not a silver bullet
- When should hash_equals() be mandatory?
- What about performance?
- Conclusion
This article explains why == and === are not safe for security-sensitive comparisons, what a timing attack is, and how hash_equals() is specifically designed to solve this problem — in a practical, real-world context.
How string comparison works in PHP
Consider the following code:
if ($input === $secret) {
// authenticated
}
When PHP compares two strings, it checks them character by character from left to right. The moment it finds a mismatch, the comparison stops and returns false.
This behavior is efficient, but it is exactly what creates a security risk.
What is a timing attack?
A timing attack is a class of attacks that relies on measuring how long an operation takes to execute.
Because PHP stops comparing strings as soon as it finds a mismatch:
- A string with 1 correct leading character takes slightly longer
- A string with 5 correct leading characters takes even longer
- A fully correct string takes the longest
If an attacker can send many requests and measure response times precisely, they can gradually infer the secret value one character at a time.
This is especially dangerous for:
- API keys
- HMAC signatures
- Webhook secrets
- Authentication tokens
The real issue is timing, not == vs ===
Some developers assume that using === is enough because it performs strict comparisons. In reality:
==is worse due to type juggling===is logically correct, but still leaks timing information
Neither operator performs a constant-time comparison.
What does hash_equals() do differently?
hash_equals() is designed specifically for comparing secrets.
Its key properties are:
- It compares the entire string, even if a mismatch is found early
- Execution time does not depend on how many characters match
- It significantly reduces the risk of timing attacks
In simple terms: correct and incorrect comparisons take nearly the same amount of time.
Incorrect comparison example (do not use)
if ($api_key === STORED_API_KEY) {
// Not safe for secrets
}
This code works functionally, but it is unsafe if the endpoint can be probed with repeated requests.
Correct comparison example (recommended)
if (hash_equals(STORED_API_KEY, $api_key)) {
// Safer comparison
}
This approach should be used for:
- API keys
- Webhook secrets
- Internal tokens
- HMAC signatures
hash_equals() is not a silver bullet
It is important to understand what hash_equals() does not protect against:
- Leaked or exposed API keys
- Secrets embedded in frontend code
- Secrets passed via URLs
hash_equals() solves one specific problem: leaking information through execution time.
When should hash_equals() be mandatory?
You should treat hash_equals() as mandatory whenever you compare:
- Any value coming directly from an HTTP request
- Any secret used for authentication
- Any signature used for verification
If in doubt, use hash_equals(). There is no valid reason not to.
What about performance?
Performance impact is negligible.
The cost of hash_equals() is trivial compared to:
- Network latency
- Database queries
- WordPress bootstrapping
Trading a tiny amount of CPU time for significantly better security is always worth it.
Conclusion
Using == or === to compare secret values is a long-standing habit in PHP development. It is not logically wrong, but it is security-unsafe.
hash_equals() exists for a very clear reason: to prevent timing attacks. If you are building APIs, webhooks, or any authentication mechanism, treat hash_equals() as the standard — not an optional improvement.
Good security does not come from complex code, but from using the right tool for the right problem.
Comments