FlawOpen

flawopen.com/Type Juggling/PHP ==

PHP == est-il sûr pour comparer des hashes ?

CWE-697: Incorrect ComparisonPage de référence
Réponse courte

Non. Avec ==, PHP compare numériquement deux chaînes numériques ; des chaînes distinctes en notation scientifique peuvent donc être égales même sous PHP 8. Validez le type de l’entrée. Utilisez === pour les valeurs exactes et hash_equals() pour les jetons secrets ou les MAC, afin d’éviter les fuites temporelles liées au contenu.

VULNÉRABLE
<?php
// Both strings are numeric and represent zero
$storedHash = "0e462097431906509019562988736854";
$submittedHash = "0e987654321";

// == returns true on PHP 7 and PHP 8
if ($storedHash == $submittedHash) {
    login();
}

// Request data may also have the wrong type:
$submitted = $_POST['token'] ?? null;
if ($storedHash == $submitted) {
    login();
}
// ?token[]=x supplies an array, not a string.
CORRIGÉ
<?php
$submitted = $_POST['token'] ?? null;
if (!is_string($submitted)) {
    http_response_code(400);
    exit;
}

// For secret tokens or MACs:
$tokenIsValid = hash_equals($storedToken, $submitted);
if ($tokenIsValid) {
    login();
}

// For non-secret identifiers, strict
// comparison checks both type and value:
$idsMatch = ($expectedId === $submitted);

// Passwords require password_verify(),
// not a hand-written hash comparison:
$passwordIsValid = password_verify($plain, $storedPasswordHash);

Le problème des « magic hashes »

PHP compare numériquement deux chaînes numériques avant et après PHP 8. Un hash hexadécimal commençant par 0e et suivi uniquement de chiffres est une chaîne valide en notation scientifique dont la valeur est zéro. Ainsi, 0e462097431906509019562988736854 et 0e987654321 sont différents, mais représentent tous deux zéro et sont égaux avec ==. PHP 8 a modifié les comparaisons chaîne-nombre, pas ce cas entre deux chaînes numériques.

PHP 8 a modifié les comparaisons entre un nombre et une chaîne non numérique : il compare désormais leurs formes textuelles au lieu de traiter la chaîne comme zéro. Il n’a pas supprimé la comparaison classique de deux chaînes numériques 0e…. N’utilisez pas == pour l’authentification, quelle que soit la version.

Les types de requête font partie de la frontière

Les paramètres d’une requête ne sont pas nécessairement des chaînes. Une requête telle que ?token[]=x produit un tableau. Le transmettre à une API de chaînes peut provoquer une erreur de type ou un chemin inattendu selon la version de PHP et la fonction. Exigez une chaîne à la frontière de la requête avant toute comparaison ou appel à hash_equals().

La défense durable comporte deux étapes : valider le type à la frontière, puis choisir une comparaison adaptée. === vérifie le type et la valeur ; hash_equals() compare les secrets sans fuite temporelle dépendant du contenu.

Questions fréquentes

PHP 8 corrige-t-il entièrement le problème ?

Non. PHP 8 a modifié les comparaisons chaîne-nombre, mais deux chaînes numériques restent comparées numériquement. Des chaînes 0e… différentes peuvent encore correspondre. Utilisez === pour les valeurs typées exactes ou hash_equals() pour les secrets et validez les types sur chaque version prise en charge.

Pourquoi hash_equals() plutôt que === ?

PHP décrit hash_equals() comme sûre contre les attaques temporelles liées au contenu ; une comparaison ordinaire peut durer différemment selon la position de la première différence. Passez le secret connu en premier, puis la chaîne utilisateur. Pour les mots de passe, utilisez password_verify().

Cela existe-t-il dans d’autres langages ?

JavaScript présente aussi des surprises avec == ; === y est donc généralement recommandé par défaut. La forme spécifique du magic hash vient de la comparaison PHP entre chaînes hexadécimales et notation scientifique.

Références

À voir aussi Rust : le bloc unsafe est-il sûr ? strncpy() est-elle plus sûre que strcpy() ? CWE vs CVE : quelle est la différence ? Mass assignment : risques et listes de champs autorisés Fuite d’état serverless : isoler les données par requête