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 · Tinggi
Riset Keamanan · FlawOpen

RubyGems: kebocoran kunci API melalui cache CDN

Cacat cache gzip/CDN mengekspos kunci RubyGems. Simak perbaikan dan pemulihan; peran agen AI dalam kampanye terpisah belum dipastikan.

💡 Penjelasan Sederhana (ELI5)

Bayangkan loket yang membuat kunci induk baru untuk setiap pelanggan terverifikasi, lalu menaruh salinannya di baki bersama karena mesin pengemas mengubah label amplop. Orang berikutnya di loket yang sama dapat mengambil kunci pelanggan sebelumnya tanpa menunjukkan identitas. Perbaikannya mencegah cache bersama menyimpan respons privat dan memisahkan respons berdasarkan otorisasi.

Konsep Kunci & Istilah

Legacy API key
Kredensial RubyGems yang berumur panjang dengan izin pengelolaan gem yang luas. Advisory menyebut kunci ini dapat menerbitkan versi gem baru, menarik versi, menambah pemilik, serta mengubah webhook atau setelan trusted publisher; kunci ini tidak dapat digunakan untuk masuk ke akun atau menulis ulang rilis yang sudah terbit.
Shared cache / POP
Point of Presence (POP) CDN menyimpan respons untuk digunakan kembali oleh permintaan berikutnya. Jika respons terautentikasi berisi rahasia tetapi tidak memiliki kebijakan cache privat dan kunci cache yang memperhitungkan otorisasi, pemanggil lain dapat menerima respons itu.
Compression middleware
RubyGems menyatakan Accept-Encoding: gzip memicu interaksi middleware Rack sehingga respons tidak memiliki direktif private dan Vary: Authorization. Permintaan biasa tanpa gzip tidak menunjukkan perilaku header yang sama.
CWE-200 and CWE-524
GitHub Security Advisory mencantumkan CWE-200 untuk paparan informasi sensitif dan CWE-524 untuk penggunaan cache yang berisi informasi sensitif. Keduanya menjelaskan kebocoran dan batas cache; tidak ada CVE yang ditetapkan.
RubyGems package-flood attribution
RubyGems mengonfirmasi penerbitan paket yang disalahgunakan pada Mei 2026, tetapi tidak dapat memastikan apakah agen AI membuat atau menerbitkan paket tersebut. Pisahkan kampanye tersebut dari cacat cache: sumber tidak membuktikan kampanye menyebabkan kunci terekspos.

Pemetaan kerangka kerja

  • Referensi CWE untuk kelemahan yang dijelaskan dalam panduan ini: CWE-200 CWE-524

Kronologi Insiden

2020-12-10

RubyGems 3.2.0 mengubah alur gem signin standar menjadi pembuatan kunci berskala terbatas, tetapi server tetap menyediakan rute GET lama.

2026-07-06

Truffle Security melaporkan masalah cache kepada RubyGems.

2026-07-09

RubyGems menerapkan perbaikan header dan menghapus objek cache Fastly yang terdampak.

2026-07-22

RubyGems menerbitkan GHSA-9j48-x3c3-mrp2 dan menyatakan semua kunci lama telah dicabut. GHSA memberi nilai 7,3 High; RubyGems melaporkan nilai lingkungan keseluruhan 7,2 High.

2026-09-11

RubyGems menjelaskan secara terpisah kampanye penerbitan paket bulan Mei dan menyatakan tidak dapat mengatribusikannya kepada agen AI.

Alur Serangan Langkah demi Langkah

Langkah 1

Rute lama menerbitkan kredensial

Permintaan GET terautentikasi ke /api/v1/api_key membuat dan mengembalikan kunci API lama dengan izin luas. RubyGems menyebut klien gem sebelum versi 3.2.0 menggunakan rute ini; server tetap menyediakannya setelah klien baru beralih ke pembuatan kunci berskala terbatas.

Langkah 2

gzip mengubah jalur pemrosesan header

Klien meminta gzip. RubyGems mengaitkan perilaku cache dengan interaksi Rack::Deflater dan Rack::ETag: respons terkompresi hanya memiliki Cache-Control: no-cache biasa, tanpa private dan tanpa Vary: Authorization.

Langkah 3

CDN menggunakan ulang respons terautentikasi

Dalam konfigurasi Fastly yang terdampak, respons dapat tersimpan di POP hingga satu jam. Kunci cache tidak memisahkan pemanggil berdasarkan Authorization, sehingga edge dapat mengirim respons sebelumnya tanpa kembali ke RubyGems untuk mengautentikasi pemanggil berikutnya.

Langkah 4

Pemanggil lain menerima kunci

Permintaan dengan varian cache sama yang melewati POP yang sama dapat menerima kunci pengguna sebelumnya. RubyGems menyebut hal ini dapat terjadi tanpa sengaja di antara proses login dan juga menggambarkan pengambilan respons cache tanpa autentikasi. Log yang tersedia tidak menunjukkan penggunaan jahat, tetapi hanya mencakup periode terbatas.

Langkah 5

Cakupan kunci membatasi dampak

Kunci lama yang bocor dapat mengizinkan penerbitan versi gem baru, penarikan versi, perubahan pemilik, serta perubahan webhook atau trusted publisher. Rilis yang sudah terbit tidak dapat ditulis ulang. RubyGems menyatakan MFA pada UI dan API menghalangi tindakan publish, yank, dan perubahan pemilik hanya dengan kunci tersebut.

Langkah 6

Perbaikan mencegah penggunaan bersama dan mencabut kunci terekspos

Patch menambahkan Cache-Control: private, no-store, Surrogate-Control: max-age=0, dan Vary: Authorization. RubyGems membersihkan objek Fastly, mencabut kunci lama, dan menonaktifkan rute login GET. Rekomendasinya adalah kunci berskala terbatas, MFA API, dan trusted publishing untuk CI.

Kode Sumber: Rentan vs Aman

Cuplikan controller sebelum perbaikan direkonstruksi dari bagian yang dihapus dalam perbaikan publik RubyGems; header cache yang tidak aman muncul dari interaksi middleware Rack, bukan dari kebijakan cache publik yang ditetapkan oleh action ini. Callback dan nilai header yang diperkuat sesuai dengan commit d3d11c0. private, no-store mencegah penyimpanan bersama, Surrogate-Control menonaktifkan cache perantara, dan Vary: Authorization memisahkan respons jika cache tetap menyimpannya. Ini analisis kode sumber, bukan resep permintaan atau patch mandiri; terapkan perbaikan lengkap vendor dan hapus entri cache lama.

IMPLEMENTASI RENTAN
class Api::V1::ApiKeysController < Api::BaseController
  include ApiKeyable

  # Controller sebelum perbaikan tidak memiliki 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
PERBAIKAN AMAN & KUAT
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

# Ditambahkan ke Api::BaseController melalui perbaikan.
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

Daftar Periksa Penguatan Sistem Rekayasa

Sumber