Garden of KnowledgeApplied Sciences › Computer Science › Software › Security › Web Security
March 22, 2026

Type Juggling

Le type juggling exploite le comportement des langages à typage dynamique qui convertissent implicitement les types lors de comparaisons. Une comparaison “lâche” peut produire des résultats inattendus exploitables pour bypasser des vérifications.

PHP — Comparaison lâche (==)§

PHP est particulièrement affecté. L’opérateur == effectue des conversions de type avant de comparer.

Table de vérités dangereuses§

// Comparaisons qui retournent TRUE de manière inattendue
var_dump(0 == "a");           // TRUE (PHP < 8.0) — "a" converti en 0
var_dump(0 == "");            // TRUE (PHP < 8.0)
var_dump(0 == "0abc");        // TRUE — "0abc" converti en 0
var_dump(0 == false);         // TRUE
var_dump(0 == null);          // TRUE
var_dump("1" == "01");        // TRUE — comparaison numérique
var_dump("10" == "1e1");      // TRUE — 1e1 = 10.0
var_dump(100 == "1e2");       // TRUE — 1e2 = 100
var_dump("" == null);         // TRUE
var_dump("" == false);        // TRUE
var_dump([] == false);        // TRUE
var_dump([] == null);         // TRUE
var_dump("0" == false);       // TRUE
var_dump("0" == null);        // FALSE (subtilité)

// PHP 8.0+ — comportement changé (0 == "a" → FALSE)
// Mais les autres cas persistent

Bypass d’authentification§

// Code vulnérable — vérification de token avec ==
$token = hash('md5', $secret);  // Ex: "0e830400451993494058024219903391"

if ($_POST['token'] == $token) {
    // Authentifié
}

// Si le hash MD5 commence par "0e" suivi de chiffres → PHP l'interprète comme 0 × 10^n = 0
// Envoyer token = 0 → 0 == "0e8304..." → TRUE !
// Ou envoyer un autre hash "magic" commençant par 0e

// Hashes MD5 "magic" (s'évaluent à 0 en comparaison lâche)
"0e462097431906509019562988736854"  // MD5 de "240610708"
"0e830400451993494058024219903391"  // MD5 de "QNKCDZO"
"0e00275209979520536065071126608"   // MD5 de "aabg7XSs"

// SHA1 magic hashes
"0e07766915004133176347055865026311692244"  // SHA1 de "10932435112"
// Autre bypass — si password_verify est remplacé par ==
$stored = "0";  // Hash stocké mal formaté
if ($stored == hash_password($input)) {
    // Si l'entrée produit un hash commençant par 0 → bypass
}

// Type juggling sur les comparaisons de tableau
var_dump("php" == 0);   // TRUE (PHP < 8)
var_dump([1,2,3] == 0); // FALSE

Bypass de strcmp()§

// Code vulnérable
if (strcmp($_POST['password'], $real_password) == 0) {
    // Authentifié
}

// Si password est un tableau : strcmp(array, string) → NULL
// NULL == 0 → TRUE !
// Envoyer : password[]=anything (PHP convertit en tableau)

Type juggling avec JSON et json_decode§

// Code vulnérable — authentification via JSON
$data = json_decode($json_input);

if ($data->password == $stored_password) {
    // Authentifié
}

// Envoyer : {"password": 0} → 0 == "anystring" → TRUE (PHP < 8)
// Envoyer : {"password": true} → true == "anystring" → TRUE

Contournement de switch§

// switch utilise == implicitement
switch ($role) {
    case "admin":
        // ...
    case "user":
        // ...
}

// Si $role = 0 → 0 == "admin" → TRUE (PHP < 8) → accès admin !

JavaScript§

// == (abstract equality) — conversions implicites
0 == ""           // true
0 == "0"          // true
0 == false        // true
"" == false       // true
null == undefined  // true
null == false      // FALSE (subtilité)
NaN == NaN         // FALSE (NaN n'est jamais égal à lui-même)

[] == false        // true ([] converti en "" puis en 0)
[] == 0            // true
[""] == false      // true
["1"] == 1         // true
["1","2"] == "1,2" // true

// parseInt — conversions inattendues
parseInt("10abc")  // 10 (s'arrête au premier non-numérique)
parseInt("0x1a")   // 26 (hex)
parseInt("010")    // En ES5 : 8 (octal) ! → utiliser parseInt("010", 10)

Bypass en Node.js§

// Comparaison de hashes avec ==
const hash = crypto.createHash('sha256').update(secret).digest('hex');
// Si hash commence par "0e..." → avec un JSON {"hash": 0} → 0 == "0e..." → true
// (moins fréquent en JS car parseInt différent)

// Type coercion avec des objets
const obj = {valueOf: () => 1};
obj == 1  // true
obj == true  // true

// null et undefined
null > 0   // false
null == 0  // false
null >= 0  // TRUE (inconsistance JavaScript !)

Python§

# Python est plus strict mais a quelques cas
0 == False   # True
1 == True    # True
0 == 0.0     # True
"" == False  # False (Python est strict ici)

# Égalité de None
None == False  # False
None == 0      # False

# Mais
bool([]) == False  # True
bool("") == False  # True
bool(0) == False   # True

# Flask/Django — confusion de types dans les comparaisons de sessions
# Si la session contient is_admin = "0" (string)
# et que le code fait : if session['is_admin'] == 0 → False (OK)
# mais : if not session['is_admin'] → "0" est truthy → pas de bypass ici
# Cependant : if session['is_admin'] == False → "0" != False en Python → OK

Cas réels et impact§

WordPress < 3.8.4 (2014) :
  Comparaison lâche du hash de confirmation d'email
  → Bypass via magic hash "0e" → account takeover

Drupal CVE-2019-6339 :
  Type juggling dans la comparaison de tokens de réinitialisation
  → Bypass via hash magic

CTF — challenges courants :
  JSON {"admin": true} envoyé à un endpoint qui compare avec ==
  PHP : paramètre tableau au lieu d'une string pour bypasser strcmp
  Hash magic : soumettre un "token" qui avec == vaut 0

Contre-mesures§

// PHP — toujours utiliser === (comparaison stricte de type ET valeur)
if ($_POST['token'] === $token) { ... }   // Sûr
if (strcmp($a, $b) === 0) { ... }         // Sûr (=== 0, pas == 0)

// Vérifier le type avant de comparer
if (!is_string($_POST['password'])) {
    die("Type invalide");
}

// hash_equals() pour comparer des tokens (temps constant + strict)
if (hash_equals($expected_token, $_POST['token'])) { ... }

// password_verify() pour les mots de passe (jamais ==)
if (password_verify($_POST['password'], $stored_hash)) { ... }

// json_decode avec l'option assoc=true et valider les types
$data = json_decode($input, true);
if (!is_string($data['password'])) {
    die("Type invalide");
}
// JavaScript — toujours utiliser ===
if (input === expected) { ... }  // Strict equality

// Node.js — crypto.timingSafeEqual pour les tokens
const crypto = require('crypto');
const a = Buffer.from(userToken);
const b = Buffer.from(storedToken);
if (a.length === b.length && crypto.timingSafeEqual(a, b)) { ... }
—The Gardener