flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems: fuga de claves API por caché CDN
Un fallo gzip/CDN expuso claves de RubyGems. Consulta la corrección y la recuperación; no se confirmó la autoría de IA en la otra campaña.
Imagina un mostrador que crea una llave maestra para cada cliente verificado y deja una copia en una bandeja común porque una máquina de embalaje cambió la etiqueta del sobre. La siguiente persona atendida en ese mismo mostrador podría llevarse la llave del cliente anterior sin identificarse. La corrección impide que las cachés compartidas almacenen respuestas privadas y separa las respuestas según la autorización.
Conceptos Clave y Términos
Legacy API key- Una credencial duradera de RubyGems con amplios permisos de gestión de gems. El aviso indica que podía publicar nuevas versiones, retirar versiones, añadir propietarios y cambiar webhooks o ajustes de trusted publishing; no permitía iniciar sesión en la cuenta ni reescribir una versión ya publicada.
Shared cache / POP- Un punto de presencia de la CDN almacena una respuesta para reutilizarla en peticiones posteriores. Si una respuesta autenticada con una credencial se comparte sin una política privada y una clave de caché que tenga en cuenta la autorización, otra persona puede recibirla.
Compression middleware- RubyGems explica que Accept-Encoding: gzip activó una interacción de Rack que dejó la respuesta sin la directiva private y sin Vary: Authorization. Las peticiones habituales sin gzip no mostraban el mismo comportamiento de cabeceras.
CWE-200 and CWE-524- El GitHub Security Advisory asigna
CWE-200a la exposición de información sensible yCWE-524al uso de una caché con información sensible. Describen la divulgación y el límite de caché; no se asignó un CVE. RubyGems package-flood attribution- RubyGems confirmó publicaciones abusivas de paquetes en mayo de 2026, pero dijo que no pudo determinar si agentes de IA los crearon o publicaron. Separa esta campaña del fallo de caché: las fuentes no demuestran que causara la exposición de claves.
Mapeos de marcos
Cronología del Incidente
RubyGems 3.2.0 cambió el flujo normal de gem signin para crear claves con alcance, pero el servidor mantuvo disponible la ruta GET heredada.
Truffle Security notificó el fallo de caché a RubyGems.
RubyGems desplegó la corrección de cabeceras y purgó los objetos afectados de la caché de Fastly.
RubyGems publicó GHSA-9j48-x3c3-mrp2 y dijo que revocó todas las claves heredadas. GHSA asigna 7,3 High; RubyGems informa de una puntuación ambiental global de 7,2 High.
RubyGems describió por separado la campaña de publicación de paquetes de mayo y dijo que no podía atribuirla a agentes de IA.
Flujo de Ataque Paso a Paso
Una ruta heredada emite una credencial
Una petición autenticada GET a /api/v1/api_key crea y devuelve una clave heredada con amplios permisos. RubyGems afirma que los clientes gem anteriores a 3.2.0 usaban esa ruta; el servidor la mantuvo después de que los clientes nuevos pasaran a crear claves con alcance.
Gzip cambia el camino de las cabeceras
El cliente solicita gzip. RubyGems atribuye el comportamiento de caché a la interacción de Rack::Deflater y Rack::ETag: la respuesta comprimida terminaba con un Cache-Control: no-cache básico, sin private y sin Vary: Authorization.
La CDN reutiliza una respuesta autenticada
Con la configuración afectada de Fastly, la respuesta podía almacenarse en un POP hasta una hora. La clave de caché no separaba clientes por Authorization, así que el borde podía devolver la respuesta previa sin volver al origen para autenticar al siguiente cliente.
Otro usuario recibe la clave
Una petición con la misma variante de caché que llegara al mismo POP podía recibir la clave del usuario anterior. RubyGems dice que podía ocurrir accidentalmente entre inicios de sesión y describe también consultas no autenticadas de una respuesta almacenada. No halló indicios de uso malicioso en los registros disponibles, pero solo cubrían un periodo limitado.
El alcance de la clave limita las consecuencias
Una clave heredada filtrada podía autorizar nuevas versiones de gems, retiradas, cambios de propietarios y cambios en webhooks o trusted publishers. Las versiones publicadas no podían reescribirse. RubyGems indica que MFA exigida tanto en la interfaz como en la API bloquea acciones de publicación, retirada y cambio de propietarios usando solo la clave.
La corrección evita el uso compartido y retira las claves expuestas
El parche añade Cache-Control: private, no-store, Surrogate-Control: max-age=0 y Vary: Authorization. RubyGems purgó objetos de Fastly, revocó las claves heredadas y retiró la ruta GET de inicio de sesión. Recomienda claves con alcance, MFA en la API y trusted publishing para CI.
Código Fuente: Vulnerable vs. Seguro
El fragmento vulnerable del controlador se reconstruye a partir del lado eliminado de la corrección pública de RubyGems; las cabeceras de caché inseguras surgían de la interacción del middleware Rack, no de una política pública establecida por esta acción. El callback y las cabeceras reforzadas coinciden con el commit d3d11c0. private, no-store impide el almacenamiento compartido, Surrogate-Control desactiva la caché de intermediarios y Vary: Authorization separa cualquier caché que aún almacene una respuesta. Es análisis del código fuente, no una receta de petición ni un parche independiente; aplica la corrección completa del proveedor y purga las entradas anteriores.
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
# El controlador anterior a la corrección no tenía la llamada de retorno deny_shared_cache.
def show
authenticate_or_request_with_http_basic do |username, password|
user = User.authenticate(username.strip, password)
check_mfa(user) do
key = generate_unique_rubygems_key
api_key = user.api_keys.build(
legacy_key_defaults.merge(hashed_key: hashed_key(key))
)
save_and_respond(api_key, key)
end
end
end
end
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
before_action :deny_shared_cache
def show
authenticate_or_request_with_http_basic do |username, password|
user = User.authenticate(username.strip, password)
check_mfa(user) do
key = generate_unique_rubygems_key
api_key = user.api_keys.build(
legacy_key_defaults.merge(hashed_key: hashed_key(key))
)
save_and_respond(api_key, key)
end
end
end
end
# Añadido a Api::BaseController mediante la corrección.
def deny_shared_cache
response.headers["Cache-Control"] = "private, no-store"
response.headers["Surrogate-Control"] = "max-age=0"
response.headers["Vary"] = [response.headers["Vary"], "Authorization"].compact_blank.join(", ")
end
Lista de Verificación de Seguridad para Ingeniería
- ✓Si usabas credenciales heredadas de RubyGems.org, sustitúyelas: RubyGems revocó todas. Audita versiones, retiradas inesperadas, propietarios, trusted publishers y webhooks; no deduzcas que las versiones existentes se reescribieron.
- ✓Usa claves API con alcance limitado a la gema y las acciones necesarias, activa MFA para las acciones de interfaz y API y prefiere trusted publishing/OIDC a secretos duraderos en CI. Verifica que cada flujo solo pueda publicar la gema prevista.
- ✓Para respuestas autenticadas o con secretos detrás de una CDN, configura Cache-Control: private, no-store en el origen, desactiva la caché del proxy e incluye cada dimensión de autorización en Vary/clave de caché. Prueba con compresión activa e inactiva a través de la CDN real.
- ✓Añade una prueba de integración que solicite la ruta de credenciales con Accept-Encoding: gzip y exija private, no-store, Surrogate-Control: max-
age=0y Vary: Authorization. Comprueba también que una segunda identidad no reciba la primera respuesta desde una caché compartida. - ✓Al corregir una fuga de caché, purga los objetos antes de emitir nuevos secretos; usa los registros históricos como evidencia acotada, no como prueba de que no hubo exposición. Documenta el periodo realmente observable.
- ✓Mantén precisa la atribución: RubyGems no confirmó autoría de IA en la campaña de mayo de 2026 y no halló pruebas de que los intentos de robo de claves API tuvieran éxito.
Fuentes
- RubyGems — Security advisory: Possible leak of legacy API keys via improper cache configuration
- RubyGems.org — GHSA-9j48-x3c3-mrp2
- RubyGems.org — remediation commit d3d11c0
- Truffle Security — Cache vulnerability in RubyGems
- RubyGems — API key scopes
- RubyGems — May spam-publishing campaign update
- CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
- CWE-524 — Use of Cache Containing Sensitive Information