These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
PHP SecurityMagic Hash Bypass

Magic Hash Authentication Bypass

Magic hashes are a specific subclass of type juggling that deserves its own page because the grep patterns and the test inputs are different enough to be worth a dedicated checklist.

Why

A string matching /^0+e\d+$/ is interpreted by PHP as scientific notation when one side of an == comparison is numeric: 0 * 10^digits = 0. Two different inputs whose md5 (or sha1, crc32, etc.) hashes are both 0e<digits-only> collide under ==. No preimage attack needed — these collisions are pre-discovered and listed publicly.

md5("240610708") === "0e462097431906509019562988736854";  // ← all digits after 0e
md5("QNKCDZO")   === "0e830400451993494058024219903391";  // ← all digits after 0e
 
"240610708" == "QNKCDZO";   // false  (strings differ)
md5("240610708") == md5("QNKCDZO");   // true  (both juggle to 0)

Any check shaped if (md5($input) == $expected) where $expected is a magic hash is bypassable with any other magic-hash input.

Search patterns

# Hash function output compared with ==
rg -n '(md5|sha1|hash|crc32)\s*\(' --type=php | rg ' == '
 
# Variants seen in the wild
rg -n '(md5|sha1|hash)\s*\([^)]+\)\s*==' --type=php
rg -n '==\s*(md5|sha1|hash)\s*\(' --type=php

Anything compared with === or hash_equals(...) is safe from this class. Everything else is a flag.

Test inputs (known magic-hash strings)

For MD5:

InputMD5 output
2406107080e462097431906509019562988736854
QNKCDZO0e830400451993494058024219903391
aabg7XSs0e087386482136013740957780965295
aaaXIA0e000045469718506715568090931435
0e2159620170e291242476940776845150308577824 (output of md5 on its own output — chains break)

For SHA-1:

InputSHA-1 output
aaroZmOk0e66507019969427134894567494305185566735
aaK1STfY0e76658526655756207688271159624026011393

Larger lists circulate on GitHub; these are enough for the first round of testing. If the application uses a different hash function, generate one:

for i in $(seq 1 1000000); do
  h=$(echo -n "$i" | md5sum | cut -d' ' -f1)
  [[ "$h" =~ ^0+e[0-9]+$ ]] && echo "$i -> $h"
done

Audit focus

For every hash comparison performed with ==:

  1. What is the secret? Session token, password hash (yes, still common in custom auth), CSRF token, signature, license key, password-reset token, payment-callback HMAC.
  2. Does attacker input feed either side? Either the input being hashed or the value compared against — both sides matter.
  3. Is the expected value already a magic hash? If you can pick the expected value to be a known 0e… hash (e.g. an installation that lets the admin set a token), you don’t even need a collision — you just need any of the canonical magic inputs.
  4. Even with ===, is the comparison constant-time? === short-circuits on the first differing byte and leaks length / prefix timing. For real secret comparison, hash_equals() is the right call.

In WordPress plugins and one-off PHP apps, md5($_GET['key']) == $stored is the most common shape. Always test with ?key=240610708 after grepping md5( in any plugin source.

Fix

// Bad
if (md5($_GET['token']) == $stored_hash) {  }
 
// Better
if (md5($_GET['token']) === $stored_hash) {  }
 
// Best
if (hash_equals($stored_hash, md5($_GET['token']))) {  }

For password storage specifically, password_hash + password_verify — never raw md5 / sha1.