FlawOpen

flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026

● RubyGems.org / Ruby Central · CWE-200 / CWE-524 · July 2026 · CVSS 7.3 · Hoch
Sicherheitsforschung · FlawOpen

RubyGems: API-Key-Leak durch CDN-Cache

Ein gzip/CDN-Fehler legte RubyGems-Schlüssel offen. Hier stehen Fix und Wiederherstellung; KI-Zuordnung der separaten Kampagne ist umstritten.

💡 Einfache Erklärung (ELI5)

Stellen Sie sich einen Schalter vor, der für jeden geprüften Kunden einen neuen Generalschlüssel ausgibt und eine Kopie in eine gemeinsame Ablage legt, weil eine Verpackungsmaschine das Etikett des Umschlags verändert hat. Die nächste Person am selben Schalter könnte den Schlüssel des vorherigen Kunden ohne Ausweis erhalten. Der Fix verhindert, dass gemeinsam genutzte Caches private Antworten speichern, und trennt Antworten nach der Autorisierung.

Kernkonzepte & Begriffe

Legacy API key
Ein langlebiges RubyGems-Zugangstoken mit weitreichenden Verwaltungsrechten. Laut Advisory konnte es neue Gem-Versionen veröffentlichen, Versionen zurückziehen, Eigentümer hinzufügen sowie Webhooks oder Trusted-Publishing-Einstellungen ändern; eine Kontoanmeldung oder das Umschreiben bereits veröffentlichter Releases war nicht möglich.
Shared cache / POP
Ein CDN-PoP speichert eine Antwort zur Wiederverwendung für spätere Anfragen. Wird eine authentifizierte Antwort mit einem Geheimnis ohne private Cache-Regel und ohne autorisierungsabhängigen Cache-Schlüssel geteilt, kann ein anderer Aufrufer sie erhalten.
Compression middleware
RubyGems zufolge löste Accept-Encoding: gzip eine Rack-Middleware-Interaktion aus, durch die der privaten Antwort die Direktive private und Vary: Authorization fehlten. Normale Anfragen ohne gzip zeigten dieses Header-Verhalten nicht.
CWE-200 and CWE-524
Das GitHub Security Advisory nennt CWE-200 für die Offenlegung sensibler Informationen und CWE-524 für die Verwendung eines Caches mit sensiblen Informationen. Die Einträge beschreiben Offenlegung und Cache-Grenze; eine CVE wurde nicht vergeben.
RubyGems package-flood attribution
RubyGems bestätigte missbräuchliche Paketveröffentlichungen im Mai 2026, konnte aber nicht feststellen, ob KI-Agenten die Pakete erstellt oder veröffentlicht hatten. Diese Kampagne ist vom Cachefehler zu trennen: Die Quellen belegen nicht, dass sie den Schlüsselabfluss verursacht hat.

Framework-Zuordnungen

  • CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-200 CWE-524

Vorfall-Zeitstrahl

2020-12-10

RubyGems 3.2.0 stellte den üblichen gem-signin-Ablauf auf Schlüssel mit eingeschränktem Geltungsbereich um; der Server ließ den alten GET-Endpunkt weiterhin zu.

2026-07-06

Truffle Security meldete den Cachefehler an RubyGems.

2026-07-09

RubyGems spielte den Header-Fix ein und entfernte betroffene Fastly-Cacheobjekte.

2026-07-22

RubyGems veröffentlichte GHSA-9j48-x3c3-mrp2 und widerrief nach eigenen Angaben alle älteren Schlüssel. GHSA bewertet den Fehler mit 7,3 High; RubyGems nennt einen gesamten Umgebungswert von 7,2 High.

2026-09-11

RubyGems beschrieb separat die Paketveröffentlichungskampagne vom Mai und erklärte, sie nicht KI-Agenten zuordnen zu können.

Schritt-für-Schritt Angriffsablauf

Schritt 1

Ein alter Endpunkt stellt ein Token aus

Ein authentifizierter GET-Aufruf an /api/v1/api_key erstellt einen älteren API-Schlüssel mit weitreichenden Rechten und gibt ihn zurück. RubyGems zufolge nutzten gem-Clients vor Version 3.2.0 diesen Pfad; der Server behielt ihn, obwohl neuere Clients auf Schlüssel mit eingeschränktem Geltungsbereich wechselten.

Schritt 2

Gzip verändert den Header-Pfad

Der Client fordert gzip an. RubyGems führt das Cacheverhalten auf das Zusammenspiel von Rack::Deflater und Rack::ETag zurück: Die komprimierte Antwort erhielt nur ein einfaches Cache-Control: no-cache, aber weder private noch Vary: Authorization.

Schritt 3

Das CDN verwendet eine authentifizierte Antwort wieder

In der betroffenen Fastly-Konfiguration konnte die Antwort bis zu einer Stunde an einem PoP liegen. Der Cache-Schlüssel trennte Aufrufer nicht nach Authorization; dadurch konnte der Edge-Knoten die vorherige Antwort zurückgeben, ohne RubyGems zur Authentifizierung des nächsten Clients aufzurufen.

Schritt 4

Ein anderer Aufrufer erhält den Schlüssel

Eine Anfrage mit derselben Cache-Variante über denselben PoP konnte den Schlüssel des vorherigen Nutzers erhalten. RubyGems zufolge konnte dies versehentlich zwischen Anmeldungen passieren; außerdem beschreibt der Anbieter das nicht authentifizierte Abrufen einer gespeicherten Antwort. Die verfügbaren Protokolle zeigen keinen missbräuchlichen Einsatz, decken aber nur einen begrenzten Zeitraum ab.

Schritt 5

Der Schlüsselumfang begrenzt die Folgen

Ein abgeflossener älterer Schlüssel konnte neue Gem-Versionen, Rücknahmen, Eigentümerwechsel sowie Änderungen an Webhooks oder Trusted Publishers erlauben. Veröffentlichte Releases ließen sich nicht umschreiben. RubyGems sagt, dass MFA für UI und API allein mit dem Schlüssel ausgeführte Push-, Yank- und Eigentümeraktionen blockiert.

Schritt 6

Der Fix verhindert gemeinsame Nutzung und ersetzt exponierte Schlüssel

Der Patch ergänzt Cache-Control: private, no-store, Surrogate-Control: max-age=0 und Vary: Authorization. RubyGems leerte den Fastly-Cache, widerrief ältere Schlüssel und stellte den GET-Anmeldeendpunkt ein. Empfohlen werden eingeschränkte Schlüssel, API-MFA und Trusted Publishing für CI.

Quellcode: Verwundbar vs. Sicher

Der ungepatchte Controller-Auszug wurde aus den im öffentlichen RubyGems-Fix entfernten Zeilen rekonstruiert; die unsicheren Cache-Header entstanden durch das Zusammenspiel der Rack-Middleware und nicht durch eine öffentliche Cache-Regel in dieser Aktion. Der gehärtete Callback und die Headerwerte entsprechen Commit d3d11c0. private, no-store verhindert gemeinsame Speicherung, Surrogate-Control deaktiviert Proxy-Caching und Vary: Authorization trennt Antworten in Caches, die dennoch speichern. Dies ist Quellcodeanalyse, keine Anfrageanleitung und kein eigenständiger Patch; wenden Sie den vollständigen Herstellerfix an und löschen Sie alte Cacheeinträge.

VERWUNDBARE IMPLEMENTIERUNG
class Api::V1::ApiKeysController < Api::BaseController
  include ApiKeyable

  # Im Controller vor der Korrektur gab es keinen deny_shared_cache-Callback.
  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
GEHÄRTETER SICHERHEITS-PATCH
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

# Durch die Korrektur zu Api::BaseController hinzugefügt.
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

Checkliste für Engineering & Systemsicherheit

Quellen