Un JWS porte deux jeux d’en-têtes. L’un est couvert par la signature. L’autre non. Lire l’algorithme dans le mauvais transforme un vérificateur en quelque chose qu’un attaquant configure.
La ligne
JWSVerifier::getAlgorithm() fusionnait les deux en-têtes avant de lire alg :
$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];
En PHP, l’étalement de tableaux à clés de chaînes dupliquées donne la priorité au dernier. L’en-tête non protégé vient en second. Un alg placé dans l’en-tête que personne n’a signé écrasait donc en silence celui qui était signé.
Pourquoi c’est grave
L’en-tête non protégé voyage en clair et n’est pas couvert par la signature. Quiconque relaie le jeton peut le réécrire, et la signature se vérifie toujours, puisqu’elle n’a jamais couvert cette partie.
Un attaquant peut donc choisir l’algorithme que le vérificateur emploiera, ce qui est toute la prémisse des attaques par confusion d’algorithme : présenter une clé publique RSA comme un secret HMAC, et un jeton que vous avez forgé avec cette valeur publique se vérifie.
Le correctif
alg est désormais lu uniquement dans l’en-tête protégé en intégrité. C’est ce que la spécification entend : un paramètre qui décide de la façon dont une signature est contrôlée n’a rien à faire hors de ce que cette signature couvre.
Corrigé en 3.4.10, 4.0.7 et 4.1.7, publiées le 6 juin 2026.
Ce qu’il faut en retenir
Deux choses, et aucune n’est propre à cette bibliothèque.
- Décidez l’algorithme de votre côté. Configurez le seul algorithme que vous acceptez et refusez le reste. L’en-tête ne peut alors plus influencer quoi que ce soit, quel que soit celui d’où il vient.
- Préférez la sérialisation compacte. Elle n’a pas d’en-tête non protégé du tout. Toute une classe de problèmes disparaît avec.
Et la leçon générale : fusionner deux niveaux de confiance en un seul tableau est une décision, même quand cela ressemble à une commodité.