flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems : fuite de clé API par cache CDN
Une faille gzip/CDN a exposé des clés RubyGems. Découvrez le correctif ; le lien avec la campagne distincte d’agents IA reste contesté.
Imaginez un guichet qui crée une clé maîtresse pour chaque client vérifié, puis en laisse une copie dans un bac partagé parce qu’une machine d’emballage a modifié l’étiquette de l’enveloppe. La personne suivante au même guichet pourrait récupérer la clé du client précédent sans présenter de pièce d’identité. Le correctif empêche les caches partagés de stocker ces réponses privées et distingue les réponses selon l’autorisation.
Concepts Clés et Termes
Legacy API key- Un identifiant RubyGems durable aux permissions étendues de gestion des gems. L’avis indique qu’il pouvait publier une nouvelle version, retirer des versions, ajouter des propriétaires et modifier les webhooks ou les paramètres trusted publisher ; il ne permettait ni de se connecter au compte ni de réécrire une version déjà publiée.
Shared cache / POP- Un point de présence de la CDN stocke une réponse pour la réutiliser lors de requêtes ultérieures. Si une réponse authentifiée contenant un secret est partagée sans politique privée ni clé de cache tenant compte de l’autorisation, un autre utilisateur peut la recevoir.
Compression middleware- RubyGems indique que Accept-Encoding: gzip déclenchait une interaction Rack qui laissait la réponse sans directive private et sans Vary: Authorization. Les requêtes ordinaires sans gzip ne présentaient pas le même comportement d’en-tête.
CWE-200 and CWE-524- Le GitHub Security Advisory associe
CWE-200à l’exposition d’informations sensibles etCWE-524à l’utilisation d’un cache contenant des informations sensibles. Ces faiblesses décrivent la divulgation et la frontière du cache ; aucun CVE n’a été attribué. RubyGems package-flood attribution- RubyGems a confirmé des publications abusives de gems en mai 2026, mais dit ne pas pouvoir déterminer si des agents IA les ont créées ou publiées. Distinguez cette campagne de la faille de cache : les sources n’établissent pas qu’elle ait causé une fuite de clé.
Correspondances de référentiels
Chronologie de l'Incident
RubyGems 3.2.0 a remplacé le flux standard gem signin par la création de clés à portée limitée, mais le serveur a conservé l’ancienne route GET.
Truffle Security a signalé le problème de cache à RubyGems.
RubyGems a déployé le correctif des en-têtes et purgé les objets concernés du cache Fastly.
RubyGems a publié GHSA-9j48-x3c3-mrp2 et annoncé la révocation de toutes les clés historiques. GHSA attribue 7,3 High ; RubyGems indique une note environnementale globale de 7,2 High.
RubyGems a décrit séparément la campagne de publication de gems de mai et déclaré ne pas pouvoir l’attribuer à des agents IA.
Déroulement de l'Attaque Étape par Étape
Une ancienne route émet un identifiant
Une requête authentifiée GET vers /api/v1/api_key crée et renvoie une clé historique aux permissions étendues. RubyGems indique que les clients gem antérieurs à 3.2.0 utilisaient cette route ; le serveur l’a conservée après le passage des nouveaux clients aux clés à portée limitée.
Gzip modifie le chemin des en-têtes
Le client demande gzip. RubyGems attribue le comportement du cache à l’interaction Rack::Deflater/Rack::ETag : la réponse compressée n’avait qu’un Cache-Control: no-cache simple, sans private ni Vary: Authorization.
La CDN réutilise une réponse authentifiée
Dans la configuration Fastly concernée, la réponse pouvait rester dans un POP jusqu’à une heure. La clé du cache ne séparait pas les clients selon Authorization ; le nœud périphérique pouvait donc renvoyer l’ancienne réponse sans contacter RubyGems pour authentifier le client suivant.
Un autre client reçoit la clé
Une requête utilisant la même variante de cache et passant par le même POP pouvait recevoir la clé du client précédent. RubyGems précise que cela pouvait arriver accidentellement entre deux connexions et décrit aussi l’interrogation non authentifiée d’une réponse en cache. Les journaux disponibles ne montrent pas d’usage malveillant, mais ne couvrent qu’une période limitée.
La portée de la clé limite les conséquences
Une clé historique divulguée pouvait autoriser de nouvelles versions de gems, des retraits, des changements de propriétaires et des modifications de webhooks ou de trusted publishers. Les versions publiées ne pouvaient pas être réécrites. Selon RubyGems, la MFA exigée pour l’interface et l’API bloque les actions push, yank et propriétaire avec la seule clé.
Le correctif empêche le partage et retire les clés exposées
Le correctif ajoute Cache-Control: private, no-store, Surrogate-Control: max-age=0 et Vary: Authorization. RubyGems a purgé les objets Fastly, révoqué les clés historiques et retiré l’ancienne route GET. L’équipe recommande les clés à portée limitée, la MFA API et trusted publishing pour la CI.
Code Source : Vulnérable vs Sécurisé
L’extrait du contrôleur vulnérable est reconstitué à partir des lignes supprimées dans le correctif public de RubyGems ; les en-têtes de cache dangereux provenaient de l’interaction du middleware Rack, pas d’une politique de cache publique définie dans cette action. Le callback et les valeurs d’en-tête renforcés correspondent au commit d3d11c0. private, no-store interdit le stockage partagé, Surrogate-Control désactive le cache des proxys et Vary: Authorization distingue toute réponse encore stockée. Cette analyse du code n’est ni une procédure de requête ni un correctif autonome ; appliquez le correctif complet du fournisseur et purgez les anciennes entrées.
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
# Le contrôleur avant correctif ne comportait pas de callback 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
# Ajouté à Api::BaseController par le correctif.
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
Liste de Contrôle de Sécurité pour l'Ingénierie
- ✓Si vous utilisiez des identifiants historiques RubyGems.org, remplacez-les : RubyGems a révoqué toutes ces clés. Vérifiez les versions, retraits inhabituels, propriétaires, trusted publishers et webhooks ; ne supposez pas que les versions existantes ont été réécrites.
- ✓Utilisez des clés API limitées aux gems et actions nécessaires, activez la MFA pour les opérations de l’interface et de l’API, et préférez trusted publishing/OIDC aux secrets CI de longue durée. Vérifiez que chaque workflow ne peut publier que la gem attendue.
- ✓Pour les réponses authentifiées ou contenant des secrets derrière une CDN, définissez Cache-Control: private, no-store à l’origine, désactivez le cache du proxy et incluez chaque dimension d’autorisation dans Vary/la clé de cache. Testez avec et sans compression via la vraie CDN.
- ✓Ajoutez un test d’intégration qui appelle la route des identifiants avec Accept-Encoding: gzip et vérifie private, no-store, Surrogate-Control: max-
age=0et Vary: Authorization. Vérifiez aussi qu’une seconde identité ne reçoit pas la première réponse du cache partagé. - ✓Lors de la correction d’une fuite de cache, purgez les objets avant d’émettre de nouveaux secrets ; traitez les journaux historiques comme des preuves limitées, pas comme la preuve d’une absence d’exposition. Documentez la période réellement observable.
- ✓Restez précis sur l’attribution : RubyGems n’a pas confirmé une création par IA pour la campagne de mai 2026 et n’a trouvé aucune preuve de réussite des tentatives de vol de clés API.
Sources
- 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