Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

Red Hat Quay: toma de control del registro por rotación de clave (CVE-2026-123710)

Red Hat Quay, antes de los parches de rama, permite a un llamante no autenticado instalar una clave de firma y falsificar tokens para cualquier repositorio.

Resumen

  • El servidor de claves de Red Hat Quay acepta un token de registro anónimo como credencial de rotación de clave. Cualquiera que alcance /keys puede instalar una clave de firma y falsificar tokens para todos los repositorios. El fallo es CVE-2026-123710, publicado el 11 de noviembre de 2026.
  • Un segundo fallo crítico, CVE-2026-110697, permite a un usuario que puede crear un espacio de nombres leer los tokens de robots de otros inquilinos mediante un patrón SQL LIKE sin escapar. Ambos afectan a un despliegue de Quay autoalojado anterior al parche de su rama.
  • Los dos fallos tienen una puntuación CVSS v3.1 Crítica: CVE-2026-123710 obtiene 9.8 y CVE-2026-110697 obtiene 9.9. No se conoce explotación y ninguno está en el catálogo KEV de CISA a 12 de noviembre de 2026. Red Hat afirma que ha corregido Quay.io.
  • Actualice al parche de su rama (3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 o 3.18.3). Si no puede, bloquee /keys en el proxy y revise las claves de servicio aprobadas.

¿Estoy afectado?

Está expuesto si se cumplen todas las condiciones que se aplican al fallo que esté comprobando:

  • Ejecuta un despliegue de Red Hat Quay 3 o Quay 5 autoalojado y su versión es anterior al parche de su rama.
  • Para CVE-2026-123710, el endpoint /keys es accesible desde clientes no confiables. El acceso anónimo está activado por defecto en Quay 3.18.2 (FEATURE_ANONYMOUS_ACCESS = True), y el servidor de claves acepta el token anónimo.
  • Para CVE-2026-110697, el despliegue es multiinquilino y el atacante tiene una cuenta que puede crear una organización o un espacio de nombres de usuario.

El fallo de toma de control de clave no requiere ninguna cuenta. El fallo de los tokens de robot requiere un espacio de nombres que el atacante pueda nombrar. Un registro de un solo inquilino sigue expuesto al primer fallo, porque los tokens falsificados llegan a todos los repositorios.

Rama Versiones afectadas Versión corregida
3.9 Todas las versiones anteriores a 3.9.28 3.9.28
3.10 Todas las versiones anteriores a 3.10.28 3.10.28
3.12 Todas las versiones anteriores a 3.12.24 3.12.24
3.14 Todas las versiones anteriores a 3.14.10 3.14.10
3.15 Todas las versiones anteriores a 3.15.9 3.15.9
3.16 Todas las versiones anteriores a 3.16.7 3.16.7
3.17 Todas las versiones anteriores a 3.17.7 3.17.7
3.18 Todas las versiones anteriores a 3.18.3 3.18.3

Red Hat también incluye Red Hat Quay 5 como afectado en las páginas de las CVE. Esas páginas no nombran una versión Quay 5 distinta. Los clientes de Quay 5 deben consultar las erratas de su suscripción antes de planificar la actualización.

En un despliegue de OpenShift, la etiqueta de la imagen de Quay en ejecución muestra la versión. La define el Operator, así que lea el despliegue y no la versión del Operator:

oc get deployment -A -l quay-component=quay-app \
  -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'

En un despliegue de Podman, inspeccione el contenedor en ejecución:

podman inspect quay --format '{{.Config.Image}}'

La etiqueta es la versión de Quay. Un digest sin etiqueta legible también corresponde a una versión, y skopeo inspect docker://<image> muestra las etiquetas de ese digest.

¿Cómo lo corrijo?

Red Hat indica la versión corregida de cada rama en las páginas de las CVE. Instale la imagen correspondiente o mueva la suscripción del Operator a un canal que incluya el parche, y confirme que los pods de Quay se reiniciaron con la nueva imagen.

  1. Registre la imagen actual de cada despliegue de Quay: oc get deployment -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'
  2. Actualice el registro Quay con el Operator, o descargue la etiqueta corregida de su rama y vuelva a desplegar.
  3. Confirme que los nuevos pods usan la imagen corregida: oc get pods -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}'
  4. Repita en cada instancia de Quay, incluidos los registros de recuperación ante desastres y de preproducción que comparten credenciales con producción.

Si compila el proyecto de código abierto usted mismo, el parche es el commit 094cfb024f01 en la rama master, fusionado el 2026-11-11. La etiqueta más reciente del proyecto es v3.18.2, así que ninguna versión etiquetada contiene todavía el parche. Las versiones corregidas de Red Hat son la vía soportada.

Si no puede actualizar de inmediato, reduzca la exposición con estas medidas. Ninguna sustituye al parche.

  1. Bloquee /keys en el proxy o el balanceador para que solo los componentes internos de confianza puedan alcanzarlo. Esto cierra la vía de toma de control de clave.
  2. Enumere las claves de servicio aprobadas y elimine cualquier clave que no reconozca, sobre todo una sin fecha de caducidad. Una clave eliminada puede reinstalarse mientras /keys siga accesible, así que hágalo solo con el bloqueo aplicado.
  3. Desactive el acceso anónimo si su despliegue no lo necesita. Esto elimina la fuente de token no autenticada, pero un atacante con cualquier cuenta puede seguir alcanzando el servidor de claves.
  4. Para CVE-2026-110697, restrinja quién puede crear organizaciones y rote los tokens de robot de los espacios de nombres cuyo nombre contenga un guion bajo o colisione con otro.
  5. Revise los registros de auditoría service_key_rotate y service_key_delete con fecha de caducidad nula, y el uso de tokens de robot que no coincida con la automatización esperada.

¿Cuál es la causa raíz?

Los dos fallos son errores distintos del mismo producto. Ambos convierten un valor de confianza en una decisión de autorización sin comprobar para qué se emitió ese valor.

El primer fallo está en el servidor de claves. _validate_jwt verifica la firma, la audiencia y el emisor, y después descarta la carga útil:

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 token de registro emitido por /v2/auth satisface esas tres comprobaciones. El endpoint toma la audiencia del parámetro service que envía el cliente. Firma el token con la clave de instancia:

    audience_param = request.args.get("service")
    token = generate_bearer_token(
        audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
    )

El servidor de claves trata después ese token como prueba de que el llamante puede rotar una clave de servicio. Nunca comprueba una reclamación de rotación ni que el token no sea un token de registro. put_service_key acepta la nueva clave sin caducidad, y replace_service_key copia la aprobación del firmante en la nueva clave y elimina al firmante:

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()

El segundo fallo está en el listado de robots. _list_entity_robots construye un patrón LIKE con el nombre del espacio de nombres sin escapar los caracteres comodín:

            .where(User.robot == True, User.username ** (entity_name + "+%"))

Peewee convierte ** en una comparación LIKE. En SQL, _ coincide con cualquier carácter y % con cualquier cadena. Un espacio de nombres llamado ab_c coincide por tanto con robots de abxc y de ab-c. La misma consulta une RobotAccountToken, así que las filas coincidentes llevan los tokens descifrados.

¿Cómo se explota?

Para CVE-2026-123710, el atacante necesita acceso de red a /keys y ninguna cuenta:

  1. Solicite un token de registro anónimo en /v2/auth, con el parámetro service puesto en el nombre de host de Quay para que la audiencia coincida.
  2. Genere un par de claves RSA y construya un JWK con la clave pública.
  3. Envíe el token anónimo como credencial en un PUT a /keys/services/quay/keys/<kid>, con el JWK del atacante en el cuerpo y sin parámetro expiration.
  4. El servidor de claves valida la firma, la audiencia y el emisor del token. No comprueba que el token se emitiera para rotar una clave.
  5. replace_service_key copia el estado aprobado de la clave de instancia en la clave del atacante, la guarda sin caducidad y elimina la clave de instancia.
  6. El atacante firma tokens de registro con la clave privada. Quay los acepta para cualquier repositorio, incluidos el envío y la sobrescritura.

Para CVE-2026-110697, el atacante necesita una cuenta que pueda crear un espacio de nombres:

  1. Cree una organización o un espacio de nombres de usuario cuyo nombre solo difiera de un espacio de nombres víctima en un carácter, con un guion bajo en esa posición.
  2. Solicite la lista de robots del nuevo espacio de nombres. Quay construye un patrón LIKE con el nombre y el guion bajo coincide con el carácter de la víctima.
  3. La respuesta incluye las cuentas de robot de la víctima y sus tokens descifrados.
  4. Use un token robado para descargar o subir las imágenes de la víctima. Renombrar o eliminar el espacio de nombres del atacante también puede renombrar o eliminar los robots correspondientes de la víctima.

¿Cómo se desarrolla el ataque?

Un fallo convierte un token de registro anónimo en una clave de firma de confianza, y el otro convierte un nombre de espacio de nombres en un comodín que lee los tokens de robot de otro inquilino.

flowchart LR A["El atacante pide un
token de registro anónimo"] --> B["/v2/auth lo firma con
la clave de instancia"] B --> C["El servidor de claves verifica la firma
pero no la reclamación de rotación"] C --> D["El atacante instala su JWK
sin caducidad"] D --> E["La aprobación se copia
en la clave del atacante"] E --> F["Los tokens falsificados se aceptan
para todos los repositorios"] G["Nombre de espacio de nombres
con un guion bajo"] --> H["LIKE de SQL coincide con robots
de otro inquilino"] H --> I["Tokens de robot leídos
entre inquilinos"]

¿Qué puede hacer un atacante?

Un atacante que completa la toma de control de clave puede leer y escribir todos los repositorios que sirve Quay, incluidas las imágenes privadas. Los tokens falsificados son tokens de registro normales, así que el registro no tiene motivo para rechazarlos. El atacante puede sustituir etiquetas, inyectar una imagen en una cadena CI/CD o descargar imágenes privadas para revisarlas.

La toma de control de clave también elimina la clave de instancia legítima. Una rotación posterior del operador real puede deshacerse mientras la clave del atacante siga aprobada, a menos que se instale la versión corregida y se elimine la clave maliciosa.

El fallo de los tokens de robot da al atacante los permisos del robot robado. Un robot con acceso de envío puede sobrescribir imágenes en el espacio de nombres de la víctima. Un robot con acceso de administrador puede cambiar la configuración de los repositorios. El atacante también puede renombrar o eliminar los robots de la víctima desde su propio espacio de nombres.

Ninguno de los fallos requiere interacción del usuario. CVE-2026-123710 exige que /keys sea accesible y que el acceso anónimo por defecto esté activo. CVE-2026-110697 exige una cuenta que pueda crear un espacio de nombres en un despliegue multiinquilino.

¿Por qué funciona la corrección?

El parche cierra las dos vías en un solo commit, 094cfb024f01. Añade una reclamación de rotación al servidor de claves, fija la audiencia del registro, exige una caducidad para las claves nuevas y escapa el patrón LIKE de los robots.

El servidor de claves ahora devuelve la carga útil descodificada. Exige una reclamación key_rotation que coincida con la clave instalada:

+    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 token de registro lleva reclamaciones access y context, así que se rechaza. Un token de rotación debe nombrar la nueva clave, así que un token emitido para otro fin no puede instalar una clave. La nueva clave también debe llevar una caducidad futura, y su vida útil queda limitada por la caducidad del firmante:

-    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)

La audiencia queda fijada al nombre de host configurado, así que un atacante no puede pedir a /v2/auth un token dirigido a otro servicio:

     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,
     )

El listado de robots usa ahora prefix_search, que escapa \, % y _ antes de la consulta:

-            .where(User.robot == True, User.username ** (entity_name + "+%"))
+            .where(User.robot == True, prefix_search(User.username, entity_name + "+"))

El parche también limita el reemplazo y la eliminación de claves al servicio nombrado, así que una clave de un servicio no puede reemplazar ni eliminar una clave de otro. Las pruebas añadidas en el commit reproducen la colisión por guion bajo, el cambio de nombre de usuario y la eliminación de espacios de nombres.

¿Cómo compruebo si estuve comprometido?

El servidor de claves registra las acciones sobre claves de servicio en el registro de auditoría. Busque rotaciones y eliminaciones que el operador no realizó, y claves sin fecha de caducidad:

  • Eventos service_key_rotate y service_key_delete con caducidad nula.
  • Eventos service_key_create de una clave que no reconozca.
  • Descargas o envíos de registro de un robot cuyo uso del token no coincida con su automatización.

La API de superusuario enumera las claves de servicio aprobadas. Revise la lista y los metadatos de las claves, incluida la caducidad:

curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/superuser/keys

Para el fallo de los tokens de robot, enumere los robots de los espacios de nombres cuyo nombre contenga un guion bajo y compárelos con la automatización esperada:

curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/organization/<org>/robots

No hay indicadores de red, nombres de archivo ni firmas de petición publicados para ninguno de los dos fallos. Una toma de control de clave correcta deja una clave aprobada en la base de datos, y esa clave es la prueba más sólida. Un intento fallido puede dejar solo un 400 o un 403 en /keys y ninguna entrada de auditoría.

Cronología

  • 2026-10-14: Red Hat abre el primer informe, sobre la inyección LIKE del listado de robots (CVE-2026-110697).
  • 2026-11-06: Red Hat abre el segundo informe, sobre la rotación de clave no autenticada (CVE-2026-123710).
  • 2026-11-11: Red Hat publica los dos registros CVE a las 15:02 y 17:06 UTC, y fusiona el commit de corrección a las 19:43 UTC. Red Hat afirma que Quay.io está corregido y nombra las versiones corregidas de cada rama.

Los dos informes llegaron a un parche en 28 días: 23 días del primer informe al segundo, y 5 días del segundo informe al parche.

Los dos fallos críticos llegaron al parche en 28 días desde el primer informe
Figura 1: Del primer informe al parche en 28 días. Fuente: Bugzilla de Red Hat, el commit de corrección de Quay y los registros CVE, comprobados el 12 de noviembre de 2026

Preguntas frecuentes

¿Afecta a Quay.io?

No al servicio alojado a 11 de noviembre de 2026. Red Hat afirma que ya ha corregido Quay.io. Los fallos afectan a los despliegues de Red Hat Quay autoalojados que no han aplicado la versión corregida.

¿Se necesita el acceso anónimo para la toma de control de clave?

No, pero elimina la necesidad de una cuenta. FEATURE_ANONYMOUS_ACCESS es True por defecto en Quay 3.18.2, y /v2/auth emite entonces el token que acepta el servidor de claves. Si el acceso anónimo está desactivado, un atacante con una cuenta válida puede obtener un token de registro e intentar la misma petición.

¿Puede un atacante con un token de robot robado tomar el control del registro?

Solo dentro de los permisos de ese robot. Un robot con acceso de envío puede sobrescribir imágenes en su espacio de nombres. La toma de control de clave es distinta, porque falsifica tokens para todos los repositorios, incluidos los que el atacante no puede nombrar hoy.

¿Qué versión debo instalar?

Instale la versión corregida de su rama: 3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 o 3.18.3. Si compila desde el código fuente, aplique el commit 094cfb024f01. La etiqueta más reciente del proyecto sigue siendo v3.18.2, así que una versión etiquetada todavía no contiene el parche.

¿Debo rotar los tokens de robot?

Sí, si un nombre de espacio de nombres de su despliegue contiene un guion bajo y otro espacio de nombres solo difiere en ese carácter. Rote los robots de ambos espacios de nombres y revise sus descargas y envíos recientes. No se puede distinguir un token leído de un token no utilizado.

¿Hay una solución alternativa si no puedo actualizar?

Bloquee /keys en el proxy y elimine las claves aprobadas que no reconozca. Restrinja la creación de organizaciones y rote los tokens de robot en colisión. Estas medidas reducen la exposición, pero el servidor de claves sigue confiando en un token de registro anónimo hasta que se instale la versión corregida.

Referencias

  • Página CVE de Red Hat para CVE-2026-123710, https://access.redhat.com/security/cve/CVE-2026-123710 (comprobada el 12 de noviembre de 2026).
  • Página CVE de Red Hat para CVE-2026-110697, https://access.redhat.com/security/cve/CVE-2026-110697 (comprobada el 12 de noviembre de 2026).
  • Bugzilla de Red Hat 2568072, el fallo de rotación de clave, https://bugzilla.redhat.com/show_bug.cgi?id=2568072 (comprobado el 12 de noviembre de 2026).
  • Bugzilla de Red Hat 2552237, el fallo del listado de robots, https://bugzilla.redhat.com/show_bug.cgi?id=2552237 (comprobado el 12 de noviembre de 2026).
  • Commit de corrección de Quay 094cfb024f01, https://github.com/quay/quay/commit/094cfb024f01 (comprobado el 12 de noviembre de 2026).
  • Código de Quay v3.18.2, endpoints/keyserver/__init__.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/keyserver/init.py (comprobado el 12 de noviembre de 2026).
  • Código de Quay v3.18.2, data/model/user.py, https://github.com/quay/quay/blob/v3.18.2/data/model/user.py (comprobado el 12 de noviembre de 2026).
  • Código de Quay v3.18.2, endpoints/v2/v2auth.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/v2/v2auth.py (comprobado el 12 de noviembre de 2026).
  • Código de Quay v3.18.2, data/model/service_keys.py, https://github.com/quay/quay/blob/v3.18.2/data/model/service_keys.py (comprobado el 12 de noviembre de 2026).
  • Registro CVE-2026-123710, https://www.cve.org/CVERecord?id=CVE-2026-123710 (comprobado el 12 de noviembre de 2026).
  • Registro CVE-2026-110697, https://www.cve.org/CVERecord?id=CVE-2026-110697 (comprobado el 12 de noviembre de 2026).
  • Catálogo de vulnerabilidades explotadas de CISA, https://www.cisa.gov/known-exploited-vulnerabilities-catalog (comprobado el 12 de noviembre de 2026).