49

In PHP a magic hash attack happens when loose type comparisons cause completely different values to be evaluated as equal, causing a password "match" without actually knowing the password. Here is an example:

<?php
if (hash('md5','240610708',false) == '0') {
  print "'0' matched " . hash('md5','240610708',false);
}

If you execute this you will see:

'0' matched 0e462097431906509019562988736854

Would this be possible in a JavaScript application that's using using the loose equality == operator to compare two hashes?

JavaScript is a weakly typed language, so I would naturally assume the type coercion can be taken advantage of and therefore present various security holes.

Peter Mortensen
  • 885
  • 5
  • 10
drewiepooey
  • 599
  • 1
  • 4
  • 7
  • 6
    Yet another reason to use prebuilt, battle-tested libraries for this kind of thing. – corsiKa Sep 07 '20 at 20:32
  • 13
    Could you please include an outline of “this attack” directly into your question to prevent the question from becoming meaningless if the link changes or the page vanishes? – Konrad Rudolph Sep 08 '20 at 09:01
  • 25
    For those wondering, this "works", because the output of this specific hash is a string which looks like a number in scientific notation with a mantissa of 0, so if evaluated as a number, its value is `0 * 10^462097431906509019562988736854` which is of course `0`. The issue here is that php will take two strings and convert them to numbers for comparison, so `'0' == '0e462097431906509019562988736854'` because `0 == 0e462097431906509019562988736854`. – jcaron Sep 09 '20 at 08:56
  • 6
    Of all programming languages in the world, two specimens with *insane* implicit type conversion rules have managed to become (a) the most used web server language (PHP) and (b) the most used web client language (JavaScript). Given the importance of the web to our society nowadays, that fact never ceases to amaze me. – Heinzi Oct 08 '20 at 08:37
  • 1
    Why would anyone compare a password hash against `0` to start with ? – Julien Oct 08 '20 at 15:03
  • @Julien usually in real world situations hashes two scientific-notation zeros (like `0e111` and `0e222`) are compared, not two decimal-notation `0`'s. That notation computes to `0` via implicit type casting, if either of operands are converted to integers. That may accidentally happen when *not* enforcing types. – hegez Oct 09 '20 at 08:05
  • @hegez agreed, I would find the question more to the point if comparing to something more specific like `'0e123456789'` i.e. starting with `'0e'` ... as `var_dump(hash('md5','240610708',false) == "0e123456789"); // bool(true)` and `var_dump(hash('md5','240610708',false) == "0g123456789"); // bool(false)` which reduces the number of so called "magic hashes", but agreed again they do exist – Julien Oct 09 '20 at 08:41
  • @Julien in PHP, `'0e111' == '0e222'` is true, even when both operands are strings. This usually happens while comparing password hash from database to hash of client-provided password, and those are usually strings. The question is about Javascript, which requires either of operands to be integer in comparison time to be vulnerable. The usual DB scenario is still possible with Node.js, likeness of happening rises when programming crypto code yourself. – hegez Oct 10 '20 at 13:14

2 Answers2

63

Being loosely typed with a crazy == operator, JavaScript is vulnerable to type juggling. But it is not as vulnerable as PHP.

Here are a few things that are equal in PHP, but not in JavaScript:

  • '0e111' == '0e222' Even though both are strings, PHP will treat them as numbers. JavaScript needs one of the operands to be a number before it tries to coerse anything into a number.
  • '0eaaa' == 0 PHP will interpret anything beginning with a number as a number, while JavaScript will not. Note that even PHP needs the other operand to be a number in this case.

However, this will be equal in both PHP and Javascript:

  • '0e111' == 0 One operand is a string containing only digits after the 0e (very unlikely that will happend at random), and the other must be an actual number (not just a string looking like a number).

This makes it harder to find type juggling vulnerabilities with hashes in JavaScript. That doesn't mean they don't exist, though. Use ===.

Anders
  • 65,052
  • 24
  • 180
  • 218
  • 6
    I think in your second example you meant '0xaaa'. – Edheldil Sep 07 '20 at 18:46
  • 4
    @Edheldil I mean `e` not `x`. JS never interprets `x` as a number. PHP does interpret the string `0xaaa` as a number. Note that a hex encoded hash will never contain `x`. I may be missing something here, though. – Anders Sep 08 '20 at 13:34
  • 3
    @Edheldil All these are strings which can be the hex output of a hash (only contain `0-9a-f`) **and** look like scientific notation numbers (`xey` which means `x * 10^y`) **and** where the mantissa (the part before the `e`) is 0 (so that `0ewhatever = 0 * 10^whatever = 0`). So they all start by `0e` followed by (decimal) digits only. The question then is whether a given language will automatically convert a **string** contain such a sequence to a number, which has then value 0. – jcaron Sep 09 '20 at 08:48
  • 2
    `'0whatever' == 0` due to the string being turned into an int. – Cees Timmerman Sep 09 '20 at 10:01
  • 1
    @CeesTimmerman is that in PHP? It evaluates to `false` if I try it in JS. If I try `Number('0whatever')` I get `NaN`, not `0`, which explains the equality being false. – Ryan1729 Sep 09 '20 at 20:45
  • @CeesTimmerman Good point, I have tested to confirm and updated my answer accordingly. – Anders Sep 09 '20 at 22:32
  • *"JavaScript needs one of the operands to be a number before it tries to coerse anything into a number."* Nitpick: That's not quite true. It also happens for boolean values. Probably not relevant in this context though. See https://felix-kling.de/js-loose-comparison/ – Felix Kling Sep 11 '20 at 11:57
  • @Anders "JS never interprets x as a number. PHP does interpret the string 0xaaa as a number." not sure exactly what you meant here but `'0x123' == 0x123` is true in JS. But of course you're right that that's not a valid hash. – Alex Hall Oct 08 '20 at 22:12
  • 1
    @AlexHall Yeah, never interpret as a number isn't really correct. My point is that it only works if it's a full match, and then you don't need type juggling. (That is, `'0x123' != 0x321`.) – Anders Oct 09 '20 at 07:56
22

Yes, sure. Magic hash attacks are also possible in JavaScript.

The JavaScript operator == means equal after type juggling.

0e123 is a valid representation of a number (in scientific notation)

If clients could control the type and value of the hash passed to the server, passing the number 0 would force JavaScript to cast magic hash to the number, resulting in number comparison:

'0e462097431906509019562988736854' == 0 -> true

The behaviour of the == operator can be seen in this equality table.

Peter Mortensen
  • 885
  • 5
  • 10
kupihleba
  • 387
  • 1
  • 7
  • 10
    One of the many reasons why it is a very useful habit for JS developers to always type `===` (strict equality) by default, and only use `==` when you know you are going to need to compare values with different type representations. But it is usually a good idea to architect your software in a way which avoids comparisons of different types. The rules of JS equality are so arcane that nobody can be expected to memorize them. It's usually better to do explicit conversions so you can use `===`. – Philipp Sep 07 '20 at 10:39
  • @Philipp completely right, and I would add that JS developpers should use `===` every time except when `==` is explicitly wanted for a valid reason, because `===` is also faser – Kaddath Sep 08 '20 at 07:58
  • The Magic-Hash Attack as described in the link from the OP will NOT work in JavaScript! JavaScript will only auto-convert types, if both values are of different types - as long as you compare two hashes of type string, JS will never convert anything (even if using ==) - as it stands I would say your answer is misleading – Falco Sep 08 '20 at 09:12
  • 3
    @Kaddath `==` is exactly as fast as `===` when both operands have the same type, and there's nothing wrong with using it when you know that both operands have the same type. You only got a problem when you don't know what types the operands have. – Bergi Sep 08 '20 at 13:48
  • 2
    @Bergi I find it safer for code maintainability/reuse, you may know that they have the same type, but the ones that follow you may not know/ forget it.. I may be wrong though – Kaddath Sep 08 '20 at 14:01
  • 2
    @Bergi If both are equally as fast, there is no reason to use anything other than the safest one. – Ian Kemp Sep 09 '20 at 16:12
  • @Bergi You should always know what type the operands have. If you are not sure about the type, an explicit conversion (e.g. `'' + x` or `+x`) is better then `==`. To be honest, the best advice is to use typescript if possible. – Sulthan Sep 11 '20 at 20:47
  • 1
    @Sulthan Totally agree. The only point I was trying to make is that "*because `===` is also faser*" is not a valid argument. – Bergi Sep 11 '20 at 21:44