PHP’s “Magic Hash” Vulnerability (Or Beware Of Type Juggling)

A while back, I noticed a flurry of activity around a somewhat obvious outcome of PHP’s type juggling antics. As the snowball gathered pace and grew, it’s being more widely reported as a vulnerability dubbed “Magic Hashes”. What is this mysterious potential vulnerability in PHP applications?

The vulnerability is a straightforward outcome of PHP’s type juggling antics. As you might recall, when comparing values for equality, PHP applies a set of rules to “juggle” types according to specific rules. For example, the string “12apples” can be prodded into an integer 12. For this reason the string “12apples” and 12 are considered equal. This holds true when using “==” and not “===” which is what you should be using to avoid this.


When we speak of “magic hashes”, we’re talking about the hexadecimal output from hashing algorithms like MD5 and SHA1 where the returned string takes on a form that can be interpreted as a float by PHP because it looks like an exponent, e.g. 0e4 (there can be any number of subsequent digits). We then find a second interesting juggle manouver. When the string is interpreted as a float, and compared to an integer, the float is then converted to the integer 0. So the integer 0 is always equal to the float 0e4 derived from the string “0e4”.

The novelty factor making this such a popular topic right now, is that this also works when BOTH sides of the comparison are strings. For example the string “0e4” is equal to “0e42”. It’s actually counterintuitive when you’re used to the more common integer rules which don’t allow for this.

Summing it up, hashes in the form ^0+e\d*$ are always equal to the any other value equivalent to zero, e.g. the integer 0 or the string “0”. If a hacker can pass in a digit value that is used directly in an equals comparison (using “==”) against a hash, there’s a tiny chance they’ll get lucky and be compared against a hash in the exponent form. The solution is perform comparisons where type must be preserved, i.e. use “===” by default unless you have an extremely good reason not to. Even better, prevent leaking timing information by using a fixed-time string comparison function (all good frameworks have one!) or the hash_equals() function if available.

That’s Not Scary Enough!

At times like this, while I applaud getting out the word about the mysterious ways in which PHP’s type juggling can screw you, I also worry about the fixation on such a narrow point with equally narrow data. That hashes might look like exponents which might, in turn, equate to zero is just one tiny slice of the actual problem: PHP has rules to turn all strings into integers, and there are many more integers other than 0.

For example, the string “10” is also equal to “1e1”, and the string “330” is equal to “33e1”. The string “666” is equal to “666e0”. Focusing merely on things beginning with “0e” or being exclusively equal to zero is short-sighted. PHP’s actual documented pattern for recognising floats of this form is “[+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM})” where {LNUM} refers to an integer/long and {DNUM} refers to any float/double. If you want to document occurrences in various hashing algorithms, modify the pattern to fit within the limits of what those algorithms output. Now you can see that something that appears improbable at first might well be a bit more probable.

Ignoring even this obvious extension of the original “magic hashes” information, there remains all of the other type juggling rules that PHP throws at you. While we’re familiar with receiving integers into PHP from user input as strings, this is not a reliable assumption when input can arrive from a variety of sources. JSON, for example, supports integers and floats which are converted into their PHP equivalents by the json_decode() function. This brings us back to the integer 12 being equal to the string “12apples”, or the integer 0 being equal to basically every string that can’t otherwise be converted to a related integer or float.

I’m Afraid Of Shadows…

…and you should be. When it comes to comparing any external input against something used in a security setting, whether it be passwords, access tokens, CSRF tokens or whatever you’re using those strings of bytes for, you should immediately default to applying a priority list of comparison methods. One which never features “==” out of fear of what PHP’s type juggling might do, and one which is secured against Timing Attacks:

That list is relatively simple, so in order of preference:

  1. Use PHP 5.6’s hash_equals() function whenever possible.
  2. If you need to support PHP 5.5 or less, use Zend\Crypt\Utils::compareStrings() from Zend Framework or Symfony\Component\Security\Core\Util\StringUtils::equals() from Symfony. Other frameworks should have an equivalent function, or just borrow one of the above for your own code. There is also the php-future package to intelligently include hash_equals() only if needed.
  3. As a last resort, just use === which does not juggle between types, but it not secured against timing attacks.

Don’t forget, it’s not just your code you have to worry about. Check that your dependencies are also doing this correctly ;).