Red Hat Quay : prise de contrôle du registre par rotation de clé (CVE-2026-123710)
Avant les correctifs de branche, Red Hat Quay laisse un appelant non authentifié installer une clé de signature et forger des jetons pour tout référentiel.
En bref
- Le serveur de clés de Red Hat Quay accepte un jeton de registre anonyme comme justificatif de rotation de clé. Toute personne qui atteint
/keyspeut donc installer une clé de signature et forger des jetons pour tous les référentiels. La faille est CVE-2026-123710, publiée le 11 novembre 2026. - Une seconde faille critique, CVE-2026-110697, permet à un utilisateur qui peut créer un espace de noms de lire les jetons de robots d'autres locataires à cause d'un motif SQL
LIKEnon échappé. Les deux touchent un déploiement Quay auto-hébergé antérieur au correctif de sa branche. - Les deux failles sont notées CVSS v3.1 Critique : CVE-2026-123710 obtient 9,8 et CVE-2026-110697 obtient 9,9. Aucune exploitation n'est connue et aucune ne figure dans le catalogue KEV de la CISA au 12 novembre 2026. Red Hat indique avoir corrigé Quay.io.
- Mettez à niveau vers le correctif de votre branche (3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 ou 3.18.3). Si vous ne pouvez pas, bloquez
/keysau proxy et vérifiez les clés de service approuvées.
Suis-je concerné ?
Vous êtes exposé si les conditions qui s'appliquent à la faille que vous vérifiez sont toutes réunies :
- Vous exploitez un déploiement Red Hat Quay 3 ou Quay 5 auto-hébergé dont la version est antérieure au correctif de sa branche.
- Pour CVE-2026-123710, le point de terminaison
/keysest joignable par des clients non fiables. L'accès anonyme est activé par défaut dans Quay 3.18.2 (FEATURE_ANONYMOUS_ACCESS = True), et c'est le jeton anonyme que le serveur de clés accepte. - Pour CVE-2026-110697, le déploiement est multi-locataires et l'attaquant dispose d'un compte qui peut créer une organisation ou un espace de noms utilisateur.
La faille de prise de contrôle de clé ne demande aucun compte. La faille des jetons de robots demande un espace de noms que l'attaquant peut nommer. Un registre mono-locataire reste vulnérable à la première faille, car les jetons forgés atteignent tous les référentiels.
| Branche | Versions concernées | Version corrigée |
|---|---|---|
| 3.9 | Toutes les versions avant 3.9.28 | 3.9.28 |
| 3.10 | Toutes les versions avant 3.10.28 | 3.10.28 |
| 3.12 | Toutes les versions avant 3.12.24 | 3.12.24 |
| 3.14 | Toutes les versions avant 3.14.10 | 3.14.10 |
| 3.15 | Toutes les versions avant 3.15.9 | 3.15.9 |
| 3.16 | Toutes les versions avant 3.16.7 | 3.16.7 |
| 3.17 | Toutes les versions avant 3.17.7 | 3.17.7 |
| 3.18 | Toutes les versions avant 3.18.3 | 3.18.3 |
Red Hat liste aussi Red Hat Quay 5 comme concerné dans les pages des CVE. Ces pages ne nomment pas de version Quay 5 distincte. Les clients Quay 5 doivent donc consulter les errata de leur abonnement avant de planifier la mise à jour.
Sur un déploiement OpenShift, le tag de l'image Quay en cours d'exécution indique la version. L'Operator le définit, il faut donc lire le déploiement et non la version de l'Operator :
oc get deployment -A -l quay-component=quay-app \
-o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'
Sur un déploiement Podman, inspectez le conteneur en cours d'exécution :
podman inspect quay --format '{{.Config.Image}}'
Le tag correspond à la version de Quay. Un digest sans tag lisible correspond quand même à une version, et skopeo inspect docker://<image> affiche les étiquettes de ce digest.
Comment corriger ?
Red Hat indique la version corrigée de chaque branche dans les pages des CVE. Installez l'image correspondante ou faites passer l'abonnement de l'Operator vers un canal qui contient le correctif, puis vérifiez que les pods Quay ont redémarré sur la nouvelle image.
- Relevez l'image actuelle de chaque déploiement Quay :
oc get deployment -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}' - Mettez à jour le registre Quay avec l'Operator, ou tirez le tag corrigé de votre branche et redéployez.
- Vérifiez que les nouveaux pods utilisent l'image corrigée :
oc get pods -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}' - Répétez pour chaque instance Quay, y compris les registres de reprise après sinistre et de préproduction qui partagent des identifiants avec la production.
Si vous compilez le projet open source vous-même, le correctif est le commit 094cfb024f01 sur la branche master, fusionné le 2026-11-11. Le tag amont le plus récent est v3.18.2, donc aucune version amont étiquetée ne contient encore le correctif. Les versions corrigées de Red Hat sont la voie prise en charge.
Si vous ne pouvez pas mettre à jour immédiatement, réduisez l'exposition avec ces mesures. Aucune ne remplace le correctif.
- Bloquez
/keysau proxy ou au répartiteur de charge pour que seuls les composants internes de confiance puissent l'atteindre. Cela ferme la voie de prise de contrôle de clé. - Listez les clés de service approuvées et supprimez toute clé que vous ne reconnaissez pas, en particulier une clé sans date d'expiration. Une clé supprimée peut être réinstallée tant que
/keysreste joignable, donc faites-le uniquement avec le blocage en place. - Désactivez l'accès anonyme si votre déploiement n'en a pas besoin. Cela supprime la source de jeton non authentifiée, mais un attaquant disposant d'un compte peut encore atteindre le serveur de clés.
- Pour CVE-2026-110697, restreignez la création d'organisations et faites tourner les jetons de robots des espaces de noms dont le nom contient un tiret bas ou entre en collision avec un autre.
- Examinez les entrées d'audit
service_key_rotateetservice_key_deletedont la date d'expiration est nulle, ainsi que l'usage de jetons de robots qui ne correspond pas à l'automatisation attendue.
Quelle est la cause racine ?
Les deux failles sont des erreurs distinctes dans le même produit. Toutes deux transforment une valeur de confiance en décision d'autorisation sans vérifier à quoi cette valeur a été délivrée.
La première faille se trouve dans le serveur de clés. _validate_jwt vérifie la signature, l'audience et l'émetteur, puis jette la charge utile :
def _validate_jwt(encoded_jwt, jwk, service):
public_key = jwtutil.jwk_dict_to_public_key(jwk)
try:
jwtutil.decode(
encoded_jwt, public_key, algorithms=["RS256"], audience=JWT_AUDIENCE, issuer=service
)
except jwtutil.InvalidTokenError:
logger.exception("JWT validation failure")
abort(400)
Un jeton de registre émis par /v2/auth satisfait ces trois contrôles. Le point de terminaison prend l'audience dans le paramètre service fourni par le client. Il signe le jeton avec la clé d'instance :
audience_param = request.args.get("service")
token = generate_bearer_token(
audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
)
Le serveur de clés traite ensuite ce jeton comme la preuve que l'appelant peut faire tourner une clé de service. Il ne vérifie jamais la présence d'une revendication de rotation et ne vérifie pas que le jeton n'est pas un jeton de registre. put_service_key accepte la nouvelle clé sans expiration, et replace_service_key copie l'approbation du signataire sur la nouvelle clé puis supprime le signataire :
def replace_service_key(old_kid, kid, jwk, metadata, expiration_date):
try:
with db_transaction():
key = db_for_update(ServiceKey.select().where(ServiceKey.kid == old_kid)).get()
key.metadata.update(metadata)
ServiceKey.create(
name=key.name,
kid=kid,
service=key.service,
jwk=jwk,
metadata=key.metadata,
expiration_date=expiration_date,
rotation_duration=key.rotation_duration,
approval=key.approval,
)
key.delete_instance()
La seconde faille se trouve dans la liste des robots. _list_entity_robots construit un motif LIKE à partir du nom de l'espace de noms sans échapper les caractères génériques :
.where(User.robot == True, User.username ** (entity_name + "+%"))
Peewee transforme ** en comparaison LIKE. En SQL, _ correspond à un caractère quelconque et % à une chaîne quelconque. Un espace de noms nommé ab_c correspond donc aux robots de abxc et de ab-c. La même requête joint RobotAccountToken, donc les lignes correspondantes portent les jetons déchiffrés.
Comment la faille est-elle exploitée ?
Pour CVE-2026-123710, l'attaquant a besoin d'un accès réseau à /keys et d'aucun compte :
- Demandez un jeton de registre anonyme à
/v2/auth, en réglant le paramètreservicesur le nom d'hôte Quay pour que l'audience corresponde. - Générez une paire de clés RSA et construisez un JWK pour la clé publique.
- Envoyez le jeton anonyme comme justificatif dans un
PUTvers/keys/services/quay/keys/<kid>, avec le JWK de l'attaquant dans le corps et sans paramètreexpiration. - Le serveur de clés valide la signature, l'audience et l'émetteur du jeton. Il ne vérifie pas que le jeton a été émis pour une rotation de clé.
replace_service_keycopie le statut approuvé de la clé d'instance sur la clé de l'attaquant, la stocke sans expiration et supprime la clé d'instance.- L'attaquant signe des jetons de registre avec la clé privée. Quay les accepte pour tout référentiel, y compris l'envoi et l'écrasement.
Pour CVE-2026-110697, l'attaquant a besoin d'un compte qui peut créer un espace de noms :
- Créez une organisation ou un espace de noms utilisateur dont le nom ne diffère d'un espace de noms victime qu'à un caractère, avec un tiret bas à cette position.
- Demandez la liste des robots du nouvel espace de noms. Quay construit un motif
LIKEà partir du nom et le tiret bas correspond au caractère de la victime. - La réponse contient les comptes robots de la victime et leurs jetons déchiffrés.
- Utilisez un jeton volé pour tirer ou envoyer les images de la victime. Renommer ou supprimer l'espace de noms de l'attaquant peut aussi renommer ou supprimer les robots correspondants de la victime.
Comment se déroule l'attaque ?
Une faille transforme un jeton de registre anonyme en clé de signature de confiance, et l'autre transforme un nom d'espace de noms en caractère générique qui lit les jetons de robots d'un autre locataire.
jeton de registre anonyme"] --> B["/v2/auth le signe avec
la clé d'instance"] B --> C["Le serveur de clés vérifie la signature
mais pas la revendication de rotation"] C --> D["L'attaquant installe son JWK
sans expiration"] D --> E["L'approbation est copiée
sur la clé de l'attaquant"] E --> F["Les jetons forgés sont acceptés
pour tous les référentiels"] G["Nom d'espace de noms
avec un tiret bas"] --> H["LIKE SQL correspond aux robots
d'un autre locataire"] H --> I["Jetons de robots lus
entre locataires"]
Que peut faire un attaquant ?
Un attaquant qui réussit la prise de contrôle de clé peut lire et écrire tous les référentiels servis par Quay, y compris les images privées. Les jetons forgés sont des jetons de registre ordinaires, donc le registre n'a aucune raison de les rejeter. L'attaquant peut remplacer des tags, injecter une image dans une chaîne CI/CD ou tirer des images privées pour les examiner.
La prise de contrôle de clé supprime aussi la clé d'instance légitime. Une rotation ultérieure de l'opérateur réel peut être annulée tant que la clé de l'attaquant reste approuvée, sauf si la version corrigée est installée et la clé malveillante supprimée.
La faille des jetons de robots donne à l'attaquant les permissions du robot volé. Un robot avec accès en envoi peut écraser des images dans l'espace de noms de la victime. Un robot avec accès administrateur peut modifier les paramètres des référentiels. L'attaquant peut aussi renommer ou supprimer les robots de la victime via son propre espace de noms.
Aucune faille ne demande d'interaction utilisateur. CVE-2026-123710 exige que /keys soit joignable et que l'accès anonyme par défaut soit actif. CVE-2026-110697 exige un compte qui peut créer un espace de noms dans un déploiement multi-locataires.
Pourquoi le correctif fonctionne-t-il ?
Le correctif ferme les deux voies dans un seul commit, 094cfb024f01. Il ajoute une revendication de rotation au serveur de clés, fixe l'audience du registre, exige une expiration pour les nouvelles clés et échappe le motif LIKE des robots.
Le serveur de clés renvoie désormais la charge utile décodée. Il exige une revendication key_rotation qui correspond à la clé installée :
+ if "access" in payload or "context" in payload:
+ abort(403)
+
+ return payload
+
+
+def _validate_rotation_claim(payload, kid):
+ if payload.get(KEY_ROTATION_CLAIM) != kid:
+ abort(403)
- _validate_jwt(encoded_jwt, signer_jwk, service)
+ payload = _validate_jwt(encoded_jwt, signer_jwk, service)
+ _validate_rotation_claim(payload, kid)
+
+ if signer_key.expiration_date is not None:
+ expiration_date = min(expiration_date, signer_key.expiration_date)
Un jeton de registre porte des revendications access et context, il est donc rejeté. Un jeton de rotation doit nommer la nouvelle clé, donc un jeton émis pour un autre usage ne peut pas installer de clé. La nouvelle clé doit aussi porter une expiration future, et sa durée de vie est plafonnée par l'expiration du signataire :
- if expiration_date is not None:
- try:
- expiration_date = datetime.utcfromtimestamp(float(expiration_date))
- except ValueError:
- logger.exception("Error parsing expiration date on key")
- abort(400)
+ if expiration_date is None:
+ abort(400)
+
+ try:
+ expiration_date = datetime.utcfromtimestamp(float(expiration_date))
+ except (TypeError, ValueError, OverflowError):
+ logger.exception("Error parsing expiration date on key")
+ abort(400)
+
+ if expiration_date <= datetime.utcnow():
+ abort(400)
L'audience est fixée au nom d'hôte configuré, donc un attaquant ne peut pas demander à /v2/auth un jeton visant un autre service :
token = generate_bearer_token(
- audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
+ app.config["SERVER_HOSTNAME"],
+ subject,
+ context,
+ access,
+ TOKEN_VALIDITY_LIFETIME_S,
+ instance_keys,
)
La liste des robots utilise maintenant prefix_search, qui échappe \, % et _ avant la requête :
- .where(User.robot == True, User.username ** (entity_name + "+%"))
+ .where(User.robot == True, prefix_search(User.username, entity_name + "+"))
Le correctif limite aussi le remplacement et la suppression de clé au service nommé, donc une clé d'un service ne peut pas remplacer ou supprimer une clé d'un autre service. Les tests ajoutés dans le commit reproduisent la collision par tiret bas, le changement de nom d'utilisateur et la suppression d'espace de noms.
Comment vérifier si j'ai été compromis ?
Le serveur de clés enregistre les actions sur les clés de service dans le journal d'audit. Recherchez les rotations et suppressions que l'opérateur n'a pas effectuées, ainsi que les clés sans date d'expiration :
- Événements
service_key_rotateetservice_key_deleteavec une expiration nulle. - Événements
service_key_createpour une clé que vous ne reconnaissez pas. - Tirages ou envois de registre par un robot dont l'usage du jeton ne correspond pas à son automatisation.
L'API superutilisateur liste les clés de service approuvées. Examinez la liste et les métadonnées des clés, y compris l'expiration :
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/superuser/keys
Pour la faille des jetons de robots, listez les robots des espaces de noms dont le nom contient un tiret bas et comparez-les à l'automatisation attendue :
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/organization/<org>/robots
Il n'existe aucun indicateur réseau, nom de fichier ou signature de requête publié pour l'une ou l'autre faille. Une prise de contrôle de clé réussie laisse une clé approuvée dans la base, et cette clé est la preuve la plus solide. Une tentative échouée peut ne laisser qu'un 400 ou un 403 sur /keys et aucune entrée d'audit.
Chronologie
- 2026-10-14 : Red Hat ouvre le premier rapport, sur l'injection
LIKEde la liste des robots (CVE-2026-110697). - 2026-11-06 : Red Hat ouvre le second rapport, sur la rotation de clé non authentifiée (CVE-2026-123710).
- 2026-11-11 : Red Hat publie les deux enregistrements CVE à 15:02 et 17:06 UTC, et fusionne le commit de correction à 19:43 UTC. Red Hat indique que Quay.io est corrigé et nomme les versions corrigées de chaque branche.
Les deux rapports ont abouti à un correctif en 28 jours : 23 jours du premier rapport au second, et 5 jours du second rapport au correctif.
FAQ
Quay.io est-il concerné ?
Non pour le service hébergé au 11 novembre 2026. Red Hat indique avoir déjà corrigé Quay.io. Les failles touchent les déploiements Red Hat Quay auto-hébergés qui n'ont pas appliqué la version corrigée.
L'accès anonyme est-il nécessaire pour la prise de contrôle de clé ?
Non, mais il supprime le besoin d'un compte. FEATURE_ANONYMOUS_ACCESS vaut True par défaut dans Quay 3.18.2, et /v2/auth émet alors le jeton que le serveur de clés accepte. Si l'accès anonyme est désactivé, un attaquant disposant d'un compte valide peut encore obtenir un jeton de registre et tenter la même requête.
Un attaquant muni d'un jeton de robot volé peut-il prendre le contrôle du registre ?
Uniquement dans les permissions de ce robot. Un robot avec accès en envoi peut écraser des images dans son espace de noms. La prise de contrôle de clé est différente, car elle forge des jetons pour tous les référentiels, y compris ceux que l'attaquant ne peut pas nommer aujourd'hui.
Quelle version dois-je installer ?
Installez la version corrigée de votre branche : 3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 ou 3.18.3. Si vous compilez depuis les sources, appliquez le commit 094cfb024f01. Le tag amont le plus récent est encore v3.18.2, donc une version amont étiquetée ne contient pas encore le correctif.
Dois-je faire tourner les jetons de robots ?
Oui, si un nom d'espace de noms de votre déploiement contient un tiret bas et qu'un autre espace de noms ne diffère qu'à ce caractère. Faites tourner les robots des deux espaces de noms et examinez leurs tirages et envois récents. On ne peut pas distinguer un jeton lu d'un jeton non utilisé.
Existe-t-il un contournement si je ne peux pas mettre à jour ?
Bloquez /keys au proxy et supprimez les clés approuvées non reconnues. Restreignez la création d'organisations et faites tourner les jetons de robots en collision. Ces mesures réduisent l'exposition, mais le serveur de clés continue de faire confiance à un jeton de registre anonyme jusqu'à l'installation de la version corrigée.
Références
- Page CVE de Red Hat pour CVE-2026-123710, https://access.redhat.com/security/cve/CVE-2026-123710 (vérifiée le 12 novembre 2026).
- Page CVE de Red Hat pour CVE-2026-110697, https://access.redhat.com/security/cve/CVE-2026-110697 (vérifiée le 12 novembre 2026).
- Bugzilla Red Hat 2568072, la faille de rotation de clé, https://bugzilla.redhat.com/show_bug.cgi?id=2568072 (vérifié le 12 novembre 2026).
- Bugzilla Red Hat 2552237, la faille de liste des robots, https://bugzilla.redhat.com/show_bug.cgi?id=2552237 (vérifié le 12 novembre 2026).
- Commit de correction Quay 094cfb024f01, https://github.com/quay/quay/commit/094cfb024f01 (vérifié le 12 novembre 2026).
- Source Quay v3.18.2,
endpoints/keyserver/__init__.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/keyserver/init.py (vérifiée le 12 novembre 2026). - Source Quay v3.18.2,
data/model/user.py, https://github.com/quay/quay/blob/v3.18.2/data/model/user.py (vérifiée le 12 novembre 2026). - Source Quay v3.18.2,
endpoints/v2/v2auth.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/v2/v2auth.py (vérifiée le 12 novembre 2026). - Source Quay v3.18.2,
data/model/service_keys.py, https://github.com/quay/quay/blob/v3.18.2/data/model/service_keys.py (vérifiée le 12 novembre 2026). - Enregistrement CVE-2026-123710, https://www.cve.org/CVERecord?id=CVE-2026-123710 (vérifié le 12 novembre 2026).
- Enregistrement CVE-2026-110697, https://www.cve.org/CVERecord?id=CVE-2026-110697 (vérifié le 12 novembre 2026).
- Catalogue des vulnérabilités exploitées de la CISA, https://www.cisa.gov/known-exploited-vulnerabilities-catalog (vérifié le 12 novembre 2026).