Le WordPress d'aujourd'hui, décodé pour les développeurs

Sécurité

hash_equals() pour comparer un jeton sans exposer une faille de timing

Comparer deux jetons avec === expose une extension à une attaque par mesure du temps de réponse, un risque que hash_equals() élimine par une comparaison en temps constant.

Par WordPress Développement • 30 septembre 2026 • 4 min de lecture • Aucun commentaire
hash_equals() pour comparer un jeton sans exposer une faille de timing

Pourquoi une simple égalité peut trahir un secret

En PHP, l’opérateur === appliqué à deux chaînes de caractères compare les octets un par un et s’arrête dès qu’une différence est détectée, sans poursuivre inutilement la comparaison. Ce comportement est parfaitement raisonnable du point de vue de la performance, mais il introduit une variation mesurable du temps d’exécution selon la position du premier caractère qui diffère entre les deux chaînes comparées.

Cette variation, minime pour une seule requête, devient exploitable statistiquement lorsqu’un attaquant peut envoyer un grand nombre de requêtes et mesurer précisément leur temps de réponse. En testant méthodiquement chaque position d’une chaîne secrète, caractère par caractère, il devient théoriquement possible de reconstituer un jeton ou une signature attendue sans jamais le connaître à l’avance, uniquement à partir des écarts de latence observés. C’est ce qu’on appelle une attaque par mesure du temps, ou attaque temporelle.

Ce que fait réellement hash_equals()

L'essentiel à retenir : === compare deux chaînes caractère par caractère et s'arrête au premier écart, ce qui varie le temps de réponse ; hash_equals() compare toujours l'intégralité des deux chaînes, en temps constant ; La fonction attend deux chaînes de même nature, pas un mélange chaîne et valeur nulle

hash_equals( $known_string, $user_string ) est une fonction native de PHP, disponible depuis PHP 5.6, conçue précisément pour éliminer cette variation. Sa particularité est de toujours comparer l’intégralité des deux chaînes, quel que soit le nombre de différences trouvées, de sorte que le temps d’exécution ne dépende pas de la position d’un éventuel écart.

$jeton_attendu = get_option( 'mon_extension_jeton_webhook' );
$jeton_recu    = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

if ( ! hash_equals( $jeton_attendu, $jeton_recu ) ) {
    status_header( 403 );
    exit;
}

Un détail de signature mérite l’attention : le premier argument doit être la chaîne connue et de confiance, le second la chaîne fournie par l’utilisateur ou par un tiers. Ce n’est pas qu’une convention de lisibilité : historiquement, l’implémentation garantissait le temps constant en priorité par rapport à la longueur du premier argument, ce qui rendait l’ordre des paramètres pertinent pour la robustesse de la protection.

Comportement avec des entrées inattendues

La fonction attend deux chaînes de caractères. Si l’une des deux valeurs n’est pas une chaîne, par exemple null parce que la donnée attendue est absente de la requête, PHP émet une erreur de type selon la version utilisée, ou renvoie un résultat de comparaison qui ne doit jamais être présumé sûr sans vérification préalable. La bonne pratique consiste à toujours garantir explicitement une chaîne, y compris vide, avant l’appel :

$jeton_recu = isset( $_SERVER['HTTP_X_SIGNATURE'] ) ? (string) $_SERVER['HTTP_X_SIGNATURE'] : '';

Un autre point souvent mal compris : des chaînes de longueurs différentes ne sont jamais considérées comme égales, la fonction le détecte immédiatement, mais cette détection de longueur elle-même ne doit pas fuiter d’information exploitable dans la pratique, l’essentiel du risque théorique portant sur la comparaison caractère par caractère d’une même longueur supposée.

La rapidité d’une comparaison qui s’arrête au premier écart est une qualité pour un tri de données, jamais pour vérifier un secret.

Cas concret : vérifier la signature d’un webhook entrant

Une extension qui reçoit des notifications d’un service externe accompagnées d’une signature calculée par une empreinte HMAC doit comparer la signature reçue à celle recalculée localement, jamais avec un simple opérateur d’égalité :

function mon_extension_verifier_webhook( $corps_requete, $secret_partage, $signature_recue ) {
    $signature_attendue = hash_hmac( 'sha256', $corps_requete, $secret_partage );

    if ( ! hash_equals( $signature_attendue, $signature_recue ) ) {
        return false;
    }

    return true;
}

Cette construction protège la comparaison finale, mais elle suppose aussi que $secret_partage lui-même reste confidentiel et correctement stocké : hash_equals() protège uniquement l’étape de comparaison, pas la génération ni le stockage du secret utilisé pour calculer la signature attendue.

  • Toujours utiliser hash_equals() pour comparer un jeton, une signature ou un hachage, jamais l’opérateur ===
  • Placer la valeur connue et de confiance en premier argument, la valeur fournie par un tiers en second
  • Garantir explicitement des chaînes de caractères en entrée, y compris vides, pour éviter un comportement indéterminé

Quand cette fonction n’est pas la bonne réponse

Pour comparer deux valeurs qui ne sont pas des secrets, comme deux identifiants publics ou deux noms de catégories, hash_equals() n’apporte rien et alourdit inutilement le code : l’opérateur === reste parfaitement adapté dès lors qu’aucune information sensible ne peut être déduite du temps de comparaison. Réserver cette fonction aux comparaisons de secrets évite de donner une fausse impression de rigueur généralisée là où elle n’a pas de sens, et garde le code lisible pour les cas où elle compte vraiment.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi