Contournement de signature JWS dans Authlib, corrigé dans la version 1.9.0 (CVE-2026-96760)
Jusqu'à la version 1.8.0, Authlib considère un objet JWS JSON au tableau de signatures vide comme vérifié, sans consulter la clé : CVE-2026-96760, critique (CVSS 9,1), corrigée dans la version 1.9.0.
Résumé (TL;DR)
- Authlib, la bibliothèque Python pour OAuth, OpenID Connect et JWT/JWS, acceptait un objet JWS JSON Serialization dont le tableau
signaturesest vide et renvoyait son payload comme vérifié avec succès, sans examiner la moindre signature et sans jamais consulter la clé de vérification transmise par l'appelant. La faille porte l'identifiant CVE-2026-96760 : il s'agit d'un contournement critique de la vérification de signature dansJsonWebSignature.deserialize_json(), dans le fichierauthlib/jose/rfc7515/jws.py. - Toutes les versions jusqu'à la 1.8.0 incluse sont affectées, selon le bulletin de sécurité de l'éditeur GHSA-6v38-4h7w-rj9h. Le correctif est livré dans Authlib 1.9.0, publiée le 14 octobre 2026.
- Le bulletin de l'éditeur attribue un score CVSS v3.1 de 9,1, critique ; l'enrichissement CISA ADP de l'enregistrement CVE lui attribue 9,8, critique. Les deux scores ne diffèrent que par l'impact sur la disponibilité.
- L'évaluation SSVC de la CISA indique Exploitation: none au 1er octobre 2026, et la faille ne figure pas dans le catalogue CISA KEV au 27 octobre 2026. Une preuve de concept publique est publiée dans le bulletin de sécurité.
- Passez à Authlib 1.9.0 ou à une version ultérieure. D'ici là, rejetez toute entrée JWS JSON dont le membre
signaturesest présent mais vide avant qu'elle n'atteigne Authlib, ou n'acceptez que les jetons en sérialisation compacte, qui ne sont pas affectés. - Authlib a enregistré 28 498 200 téléchargements PyPI la semaine dernière et 107 757 780 le mois dernier (pypistats, consulté le 27 octobre 2026).
Suis-je concerné ?
Vous êtes concerné si toutes ces conditions sont réunies :
- Un service Python installe le paquet
authliben version 1.8.0 ou antérieure et utilise son modulejosepour vérifier des JSON Web Signatures ou des JSON Web Tokens. - Le service accepte la JWS JSON Serialization générale, l'objet
{"payload": ..., "signatures": [...]}défini par la RFC 7515, section 7.2.1, en plus ou à la place des jetons compacts à points. - Ces jetons sérialisés en JSON atteignent
JsonWebSignature.deserialize_json(), ouJsonWebSignature.deserialize(), qui transmet les entrées de type dict et le texte JSON àdeserialize_json(). - La source des jetons est accessible à un attaquant, par exemple un corps de requête, un webhook, une file de messages ou un en-tête sur une API publique.
Vous n'êtes pas concerné dans les cas suivants :
- Vous ne vérifiez que des jetons en sérialisation compacte (
header.payload.signature) :deserialize_compact()vérifie la signature sans condition. - Vous n'acceptez que la forme JSON aplatie (
{"payload": ..., "protected": ..., "signature": "..."}) : elle emprunte une autre branche et valide son unique signature. - Vous vérifiez les jetons avec
JsonWebToken.decode(), l'appeljwt.decode()utilisé par la plupart du code JWT : il compte les points de son entrée, envoie un jeton à 2 points versdeserialize_compact()(et un jeton à 4 points vers JWE), et lèveDecodeErrorpour tout le reste, de sorte qu'il n'atteint jamaisdeserialize_json(). Dans la version 1.8.0 (authlib/jose/rfc7519/jwt.py), un objet{"payload": ..., "signatures": []}forgé ne contient aucun point et y est rejeté, alors queJsonWebSignature().deserialize()l'accepte.
| Paquet | Versions affectées | Version corrigée |
|---|---|---|
| authlib (PyPI) | toutes les versions jusqu'à la 1.8.0 incluse | 1.9.0 |
Vérifiez la version installée :
pip show authlib | grep -E "^(Name|Version)"
Vérifiez la version épinglée dans vos fichiers de verrouillage :
grep -iE '^authlib' requirements.txt requirements/*.txt 2>/dev/null
grep '"authlib"' Pipfile.lock 2>/dev/null
grep -A2 'name = "authlib"' uv.lock 2>/dev/null | grep -E 'name|version'
grep -iE 'authlib' pyproject.toml poetry.lock 2>/dev/null
Vérifiez si votre code utilise les points d'entrée affectés :
grep -rn "deserialize_json\|deserialize(" --include="*.py" . | grep -v test
Pour les conteneurs, vérifiez le conteneur en cours d'exécution, pas l'image que vous pensez avoir construite :
docker exec <container> pip show authlib | grep Version
Comment corriger la faille ?
Mettez à jour Authlib vers la version 1.9.0 ou ultérieure et recréez les conteneurs ou images serveur déployés, afin que le correctif soit présent dans le processus qui s'exécute, et pas seulement dans le dernier tag d'image.
pip install --upgrade "authlib>=1.9.0"
Si vous ne pouvez pas encore mettre à jour, filtrez l'entrée avant qu'elle n'atteigne Authlib. Rejetez tout objet JWS JSON dont le membre signatures est une liste vide : c'est le chemin de la CVE-2026-96760. Le filtre rejette aussi un membre null, qu'Authlib refuse déjà avec une TypeError ; le rejeter ici transforme seulement ce plantage en un BadSignatureError propre. Laissez passer les objets sans ce membre : il s'agit de la forme aplatie, qu'Authlib valide déjà. Faites ensuite passer chaque appel à deserialize() et deserialize_json() par un wrapper unique qui applique le filtre avant de transmettre le jeton à deserialize() :
import json
from authlib.jose import JsonWebSignature
from authlib.jose.errors import BadSignatureError
_jws = JsonWebSignature()
def _reject_empty_signatures(obj):
if "signatures" in obj and not obj["signatures"]:
raise BadSignatureError("JWS carries no signatures")
def safe_deserialize(data, key, decode=None):
if isinstance(data, dict):
_reject_empty_signatures(data)
else:
raw = data.encode() if isinstance(data, str) else data
if raw.startswith(b"{") and raw.endswith(b"}"):
_reject_empty_signatures(json.loads(raw))
return _jws.deserialize(data, key, decode)
Le wrapper reproduit la manière dont deserialize() choisit un format : les dicts et le texte JSON passent par le filtre, et les jetons compacts sont transmis directement à Authlib, qui les vérifie. Les limites de cette solution de contournement : elle ne protège que les points d'appel que vous encapsulez, donc chaque chemin qui accepte des JWS en sérialisation générale a besoin du même contrôle, et elle ne modifie pas Authlib lui-même, donc tout appel futur qui contourne votre wrapper n'est pas protégé. Elle couvre la CVE-2026-96760, c'est-à-dire le chemin des signatures vides ; les entrées compactes et aplaties échouent déjà de manière sûre dans Authlib.
Quelle est la cause racine ?
JsonWebSignature.deserialize_json() dans authlib/jose/rfc7515/jws.py initialise son indicateur de validité à True avant d'avoir vérifié quoi que ce soit, et ne le passe à False qu'à l'intérieur de la boucle sur le tableau signatures du jeton. Un JWS en sérialisation générale dont la liste signatures est vide saute donc toutes les étapes de vérification et est renvoyé comme vérifié avec succès, alors même que l'appelant a fourni une clé valide.
Le code vulnérable, lu dans les sources de la v1.8.0 (à partir de la ligne 204) :
headers = []
is_valid = True
for header_obj in obj["signatures"]:
jws_header, valid = self._validate_json_jws(
payload_segment, payload, header_obj, key
)
headers.append(jws_header)
if not valid:
is_valid = False
rv = JWSObject(headers, payload, "json")
if is_valid:
return rv
raise BadSignatureError(rv)
Trois éléments font de ce bug un contournement de la vérification plutôt qu'un plantage :
is_validvautTrueau départ. Son état par défaut affirme que l'objet est valide avant qu'aucune signature n'ait été examinée.- Le corps de la boucle, le seul endroit qui peut définir
is_valid = False, s'exécute une fois par entrée deobj["signatures"]. Une liste vide donne zéro itération, et l'indicateur conserve sa valeur initialeTrue. - La
keyde l'appelant n'est utilisée qu'à l'intérieur de_validate_json_jws(), que la boucle vide n'appelle jamais. La clé de vérification soigneusement fournie par l'application n'est jamais consultée.
Le contrat de la méthode rend l'objet renvoyé faisant foi : deserialize_json() ne signale un rejet qu'en levant BadSignatureError, donc lorsque l'appel renvoie un JWSObject au lieu de lever une exception, l'appelant considère son payload comme vérifié. Le point d'entrée élargit la portée : deserialize() transmet les entrées de type dict et tout texte JSON qui commence par { et se termine par } à deserialize_json(), tandis que les jetons compacts vont vers deserialize_compact(), qui exécute toujours algorithm.verify().
Comment la faille est-elle exploitée ?
L'attaquant soumet un payload enveloppé dans une JWS JSON Serialization générale avec un tableau signatures vide, et l'application le renvoie comme vérifié, alors que la clé de l'appelant n'est jamais consultée et qu'aucune signature n'existe dans l'objet.
La preuve de concept publique du bulletin de sécurité le démontre. La dernière ligne est ajoutée ici et transmet une vraie clé, à l'image du cas avec clé fournie dans la transcription du bulletin :
import base64
from authlib.jose import JsonWebSignature
def b64(b):
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
jws = JsonWebSignature()
payload = b'{"sub":"attacker","role":"admin"}'
forged = {"payload": b64(payload), "signatures": []}
print(jws.deserialize_json(forged, key=None))
print(jws.deserialize_json(forged, key="the-apps-real-hs256-secret")) # still accepted
Le bulletin consigne le résultat observé sur un artefact Authlib 1.8.0 installé sous Python 3.14 : l'objet forgé est ACCEPTED aussi bien avec key=None qu'avec la vraie clé.
Le diagramme retrace le chemin par lequel le payload forgé obtient la confiance de l'application :
{sub, role, scopes}"] --> B["Payload en base64url dans une
sérialisation JSON générale"] B --> C["signatures: []
aucune signature n'existe"] C --> D["deserialize_json() / deserialize()
avec la clé de l'application"] D --> E["La vérification ne s'exécute jamais :
la boucle sur signatures
fait zéro itération"] E --> F["is_valid reste True,
payload renvoyé comme vérifié"] F --> G["Identité, rôles ou autorisations
forgés acceptés"] style E stroke:#c00,stroke-width:3px
Que peut faire un attaquant ?
Un attaquant peut amener une application qui vérifie des JWS à accepter un contenu de payload arbitraire comme correctement signé, et le bulletin de sécurité ainsi que la note du CERT/CC détaillent ce que cela implique en général.
- Contournement de l'authentification : des claims d'identité forgés comme
subou un claim de privilège commerole=adminpassent pour authentiques. - Autorisation forgée : les scopes, rôles et permissions contenus dans le payload deviennent dignes de confiance.
- Injection de messages signés : les microservices qui s'authentifient mutuellement par des messages signés en JWS acceptent des messages rédigés par l'attaquant.
- Contournement de l'intégrité : tout flux de données qui s'appuie sur des signatures JWS pour prouver l'origine accepte un contenu forgé.
L'attaquant n'a besoin d'aucune clé, d'aucun identifiant et d'aucune interaction utilisateur, ce qui explique pourquoi les deux vecteurs publiés indiquent AV:N/AC:L/PR:N/UI:N. La condition limitante se situe du côté de l'application : elle doit accepter des JWS en sérialisation générale provenant d'une source non fiable et faire confiance à la valeur renvoyée comme vérifiée.
Les deux scores publiés ne diffèrent que par l'impact sur la disponibilité :
| Source | Score CVSS v3.1 | Vecteur | Disponibilité |
|---|---|---|---|
| Bulletin de l'éditeur GHSA-6v38-4h7w-rj9h | 9,1, critique | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | Aucun (A:N) |
| Enrichissement CISA ADP de l'enregistrement CVE | 9,8, critique | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | Élevé (A:H) |
L'évaluation SSVC de la CISA du 1er octobre 2026 indique Exploitation: none, Automatable: yes, Technical Impact: total. Le même enrichissement classe le défaut de vérification de signature sous CWE-347 (Improper Verification of Cryptographic Signature).
Pourquoi le correctif fonctionne-t-il ?
Le correctif ajoute un contrôle avant la boucle : un tableau signatures vide, la seule forme qui permettait à is_valid de conserver sa valeur initiale True, le force désormais à False, et un test de non-régression verrouille ce comportement. Un tableau null n'a jamais atteint le retour vérifié : itérer sur None lève une TypeError, avant comme après le correctif.
Le patch, commit 6c11a02d, fusionné dans la pull request #938 :
headers = []
is_valid = True
+
+ # invalidates cases like obj["signatures"] = None or obj["signatures"] = []
+ if not obj["signatures"]:
+ is_valid = False
+
for header_obj in obj["signatures"]:
jws_header, valid = self._validate_json_jws(
payload_segment, payload, header_obj, key
Le commentaire du code mentionne aussi None, mais pour cette entrée le contrôle ne fait que définir l'indicateur : la boucle for qui suit itère toujours sur None et lève une TypeError, comme auparavant. Le rejet propre par BadSignatureError ne s'applique donc qu'au tableau vide, pas à None.
Avec ce contrôle en place, un tableau vide force is_valid = False, la méthode aboutit donc à raise BadSignatureError(rv) et l'appelant voit un jeton rejeté. Le test ajouté, test_deserialize_json_empty_signatures, fournit {"payload": "YQ", "signatures": []} et vérifie que c'est exactement cette exception qui est levée.
Le correctif est complet pour ce chemin : un attaquant ne peut plus atteindre le retour vérifié sans qu'au moins une signature ait été validée, et la boucle de validation par signature reste inchangée, de sorte que les jetons qui portent des signatures sont vérifiés comme avant.
Comment savoir si j'ai été compromis ?
Aucun indicateur de compromission n'a été publié pour cette faille au 27 octobre 2026 : les vérifications ci-dessous sont donc des motifs dérivés de la faille elle-même, et non une liste d'IOC fournie par l'éditeur.
- Recherchez dans les corps de requête, les journaux de réception des jetons et les journaux de webhooks la signature structurelle de l'objet forgé : un corps JWS JSON dont le membre
signaturesest un tableau vide, par exemple"signatures":[]ou"signatures": []. - Examinez les journaux d'authentification et d'autorisation autour de tout JWS accepté dont les claims ont changé sans activité de signature correspondante de votre côté, par exemple une valeur d'identité ou de rôle qu'aucun émetteur légitime n'a produite.
- Établissez votre fenêtre d'exposition :
pip show authlibsur l'hôte déployé, oudocker exec <container> pip show authlib, donne la version installée, et votre historique de déploiement indique ensuite combien de temps une version antérieure à la 1.9.0 a été en service, ce qui délimite la fenêtre que votre recherche dans les journaux doit couvrir.
Chronologie
- 2026-09-23 : CVE-2026-96760 réservée par le CERT/CC (dateReserved de l'enregistrement CVE).
- 2026-09-28 : le CERT/CC publie la note VU#762428 et l'enregistrement CVE. La note décrit les versions jusqu'à la 1.7.2 incluse et indique que l'éditeur n'a pas pu être joint et qu'aucun correctif n'était disponible au moment de la rédaction.
- 2026-10-01 : le correctif est écrit (date d'auteur du commit 6c11a02d) ; l'enrichissement CISA ADP est enregistré le même jour avec Exploitation: none.
- 2026-10-14 : Authlib v1.9.0 est publiée avec le correctif, dans la pull request #938 de Leonardo Oporto.
- 2026-10-27 : la GitHub Advisory Database publie l'entrée revue GHSA-6v38-4h7w-rj9h à 14:50 UTC, en créditant zer0d4y5, uziii2208 et nguyenminhtuan28 comme auteurs du signalement ; cet article suit.
FAQ
Les JWT ordinaires en sérialisation compacte sont-ils concernés ?
Non. La forme à points header.payload.signature passe par deserialize_compact(), qui appelle toujours algorithm.verify() et lève BadSignatureError en cas de non-concordance. Seule la forme de sérialisation JSON générale avec un tableau signatures vide est concernée.
Le bug n'a-t-il d'effet que lorsque l'application ne transmet aucune clé ?
Non. La clé de vérification est utilisée à l'intérieur de _validate_json_jws(), que la boucle vide n'appelle jamais, de sorte qu'un jeton forgé passe même lorsque l'application fournit correctement une clé valide. L'extrait d'exploit de la section « Comment la faille est-elle exploitée ? » appelle deserialize_json() avec key=None puis avec une vraie clé, et la reproduction du bulletin montre l'objet forgé accepté dans les deux cas. Ce sont précisément les applications qui transmettent leur vraie clé qui sont vulnérables.
La forme JSON aplatie est-elle concernée ?
Non. Un objet doté d'un membre signature au lieu de signatures emprunte la branche aplatie, qui valide son unique signature et lève une exception en cas d'échec. Le bug provient de l'initialisation de l'indicateur dans la forme générale combinée à un tableau vide.
La faille est-elle exploitée dans la nature ?
Aucune exploitation n'est connue. L'évaluation SSVC de la CISA du 1er octobre 2026 indique Exploitation: none, et la CVE ne figure pas dans le catalogue CISA KEV au 27 octobre 2026. Une preuve de concept publique existe depuis le bulletin de sécurité : les vérifications d'exposition doivent donc partir du principe que la technique est reproductible par n'importe qui.
Pourquoi le CERT/CC mentionnait-il la 1.7.2 alors que le bulletin mentionne la 1.8.0 ?
La note du CERT du 28 septembre 2026 liste les versions jusqu'à la 1.7.2 et indique qu'aucun correctif n'existait alors. La version 1.8.0 était pourtant déjà sortie le 30 août 2026 ; les sources n'expliquent pas pourquoi la note antérieure s'arrêtait à la 1.7.2. Le bulletin de l'éditeur GHSA-6v38-4h7w-rj9h fait foi pour la plage : toutes les versions jusqu'à la 1.8.0 incluse, avec un premier correctif dans la 1.9.0, et la reproduction du bulletin lui-même a été observée sur la 1.8.0.
La mise à jour nécessite-t-elle des modifications de code ?
Non. Le correctif est interne à deserialize_json() : pip install --upgrade "authlib>=1.9.0" puis la recréation des conteneurs déployés suffisent à corriger la faille. Les applications qui conservent un préfiltre de contournement peuvent le retirer après la mise à jour, puisque la 1.9.0 échoue de manière sûre sur des signatures vides.
Trouvez les installations vulnérables d'Authlib avant un attaquant
Lancez Ostorlab SCA sur vos dépôts et vos images de conteneurs pour lister chaque projet et chaque version qui embarque encore Authlib dans une version antérieure à la 1.9.0.
Références
- Bulletin de sécurité de l'éditeur GHSA-6v38-4h7w-rj9h, avec la cause racine, la preuve de concept, la plage affectée jusqu'à la 1.8.0 et le correctif de la 1.9.0 : https://github.com/authlib/authlib/security/advisories/GHSA-6v38-4h7w-rj9h (consulté le 27 octobre 2026)
- Entrée revue GHSA-6v38-4h7w-rj9h de la GitHub Advisory Database, critique, CVSS v3.1 9,1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N), publiée le 27 octobre 2026, créditant zer0d4y5, uziii2208 et nguyenminhtuan28 : https://github.com/advisories/GHSA-6v38-4h7w-rj9h (consulté le 27 octobre 2026)
- Enregistrement CVE-2026-96760, CNA CERT/CC, publié le 28 septembre 2026, avec l'enrichissement CISA ADP : CVSS v3.1 9,8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), CWE-347 (Improper Verification of Cryptographic Signature), SSVC Exploitation: none au 1er octobre 2026 : https://cveawg.mitre.org/api/cve/CVE-2026-96760 (consulté le 27 octobre 2026)
- Note de vulnérabilité CERT/CC VU#762428, publication initiale le 28 septembre 2026, décrivant le contournement par signatures vides, les deux points d'entrée affectés, les scénarios d'impact et l'absence de correctif à la publication : https://www.kb.cert.org/vuls/id/762428 (consulté le 27 octobre 2026)
- Commit de correction 6c11a02d, « Fix empty signatures validation in JWS JSON deserialization », écrit le 1er octobre 2026 par Leonardo Oporto, avec le contrôle ajouté et le test de non-régression : https://github.com/authlib/authlib/commit/6c11a02da8730914cea08ec195f1108a40dc6733 (consulté le 27 octobre 2026)
- Pull request de correction #938 : https://github.com/authlib/authlib/pull/938 (référencée dans les notes de version de la v1.9.0, consultée le 27 octobre 2026)
- Version Authlib v1.9.0, publiée le 14 octobre 2026, qui embarque le correctif : https://github.com/authlib/authlib/releases/tag/v1.9.0 (consulté le 27 octobre 2026)
- Fichier vulnérable
authlib/jose/rfc7515/jws.pyau tag v1.8.0, avec les implémentations dedeserialize_json(),deserialize()et_validate_json_jws(): https://raw.githubusercontent.com/authlib/authlib/v1.8.0/authlib/jose/rfc7515/jws.py (consulté le 27 octobre 2026) - Chiffres de téléchargement PyPI d'Authlib, 28 498 200 la semaine dernière et 107 757 780 le mois dernier : https://pypistats.org/api/packages/authlib/recent (consulté le 27 octobre 2026)
- Catalogue CISA Known Exploited Vulnerabilities, consulté pour CVE-2026-96760 le 27 octobre 2026 (non listée) : https://www.cisa.gov/known-exploited-vulnerabilities-catalog