Signature
hash_equals(
#[\SensitiveParameter] string $known_string,
#[\SensitiveParameter] string $user_string
): boolPurpose
hash_equals() checks whether two strings are identical without letting the running time reveal where they differ. A normal === comparison can stop at the first mismatching byte, so an attacker who can measure response times precisely might learn a secret one position at a time. This is called a timing attack. hash_equals() is the tool PHP provides to reduce that risk when you compare secret values such as HMAC signatures, API tokens or password-reset tokens.
Signature
Parameters and defaults
Neither parameter has a default value; both are required.
$known_string: the string you trust and want to keep secret. Typically it is a value you computed yourself, for example withhash_hmac().$user_string: the string supplied from outside, such as a value from a URL, cookie or form.
The order matters. The manual stresses that the user-supplied string must be the second argument, because the function protects the contents of the first one.
Return value and failure cases
The function returns true when the strings are equal and false otherwise. The comparison is exact and case-sensitive, byte for byte.
One limit is documented: the strings must have the same length to match. If the lengths differ, false comes back immediately, which means the length of the known string could leak through timing. That is usually acceptable for fixed-length values such as the hex output of a hash, but keep it in mind for variable-length secrets.
Both parameters are declared as string. Pass real strings and cast deliberately when you start from another type; do not rely on loose conversion, and expect an error rather than a silent comparison if a non-string reaches the function.
Example 1: basic comparison
Two strings of equal length are compared. The first pair matches; the second differs in its last character.
echo json_encode([
hash_equals('abc123', 'abc123'),
hash_equals('abc123', 'abc124'),
]), PHP_EOL;[true,false]
Example 2: verifying an HMAC signature
The typical use is verifying a signed message. The server recomputes the signature from the message and its secret key, then compares it with the signature the client sent. The recomputed value goes first because it is the known string.
$key = 'server-secret-key';
$message = 'user=42&role=reader';
$known = hash_hmac('sha256', $message, $key);
$validSignature = $known;
$forgedSignature = hash_hmac('sha256', 'user=42&role=admin', $key);
echo json_encode([
'valid' => hash_equals($known, $validSignature),
'tampered' => hash_equals($known, $forgedSignature),
]), PHP_EOL;{"valid":true,"tampered":false}
Because both signatures are SHA-256 hex digests, they always have the same length, so the length limitation does not matter here.
Example 3: case, length and casting
This example shows three edge cases: the comparison is case-sensitive, different lengths never match, and a number must be turned into a string explicitly.
$numericId = 1234;
echo json_encode([
'caseSensitive' => hash_equals('ABC', 'abc'),
'differentLength' => hash_equals('token', 'token-2'),
'castedNumber' => hash_equals('1234', (string) $numericId),
]), PHP_EOL;{"caseSensitive":false,"differentLength":false,"castedNumber":true}
Edge cases
- Empty strings: two empty strings are equal, so
hash_equals('', '')istrue. Make sure a missing user value is never compared against an empty known value by accident; check for a missing secret before comparing. - Different lengths: the function returns
falseright away. Do not use it to hide the length of a secret. - Binary data: the strings are compared as bytes, so raw binary hashes work as long as both sides use the same representation (raw or hex).
- Sensitive parameters: the
#[\SensitiveParameter]attribute keeps the argument values out of stack traces, which helps stop secrets from appearing in logs.
Common mistakes
- Swapping the arguments. Put the secret or recomputed value first and the user input second.
- Using
==or===for secrets. These comparisons are not designed to be timing-safe. - Comparing different encodings. A hex digest never equals the raw binary digest of the same data. Normalize both to the same form first.
- Passing non-strings. Cast values such as integer IDs to strings on purpose, or better, avoid comparing them this way at all.
- Using it for password hashes. Stored passwords should be checked with
password_verify(), which does the hashing and comparison for you.hash_equals()is for comparing two already-computed values. - Expecting normalization. It will not ignore case, trim whitespace or decode anything. Prepare both strings identically before comparing.
Related functions
hash_hmac()generates a keyed hash that you typically compare withhash_equals().hash()computes a plain hash of a string.password_verify()checks a password against a stored password hash.
Recap
Use hash_equals($known, $userSupplied) whenever you compare a secret or a signature with outside input. Keep the known value first, make sure both are strings of the same encoding and length, and remember that the result is a plain boolean: true for an exact match and false for everything else.
Tested examples
echo json_encode([
hash_equals('abc123', 'abc123'),
hash_equals('abc123', 'abc124'),
]), PHP_EOL;[true,false]
$key = 'server-secret-key';
$message = 'user=42&role=reader';
$known = hash_hmac('sha256', $message, $key);
$validSignature = $known;
$forgedSignature = hash_hmac('sha256', 'user=42&role=admin', $key);
echo json_encode([
'valid' => hash_equals($known, $validSignature),
'tampered' => hash_equals($known, $forgedSignature),
]), PHP_EOL;{"valid":true,"tampered":false}
$numericId = 1234;
echo json_encode([
'caseSensitive' => hash_equals('ABC', 'abc'),
'differentLength' => hash_equals('token', 'token-2'),
'castedNumber' => hash_equals('1234', (string) $numericId),
]), PHP_EOL;{"caseSensitive":false,"differentLength":false,"castedNumber":true}