flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems:CDNキャッシュによるAPIキー漏えい
gzip/CDNキャッシュの不備でRubyGemsの旧キーが漏えいする可能性がありました。修正と復旧策を解説し、別件のAI関与は未確定とします。
本人確認を済ませた顧客ごとに合鍵を作る窓口を想像してください。包装機が封筒のラベルを変えたため、合鍵が共用トレーに置かれてしまいます。同じ窓口を次に使った人が、身分証を示さず前の顧客の鍵を受け取る可能性があります。修正では、共有キャッシュに機密応答を保存させず、認証情報ごとに応答を分けます。
主要な概念と専門用語
Legacy API key- RubyGemsのgem管理に広い権限を持つ、長期間有効な認証情報です。アドバイザリによると、新しいgem版の公開、版の取り下げ、所有者の追加、WebhookやTrusted Publisher設定の変更が可能でした。アカウントへのログインや公開済みリリースの書き換えはできません。
Shared cache / POP- CDNのPoint of Presence(POP)が応答を保存し、後続リクエストに再利用します。認証済み応答に秘密情報が含まれるのに、privateなキャッシュ方針と認証情報を考慮したキャッシュキーがなければ、別の利用者に応答が返るおそれがあります。
Compression middleware- RubyGemsによると、Accept-Encoding: gzipによってRackミドルウェア間の処理が変わり、応答にprivate指示とVary: Authorizationが付かない状態になりました。gzipを使わない通常のリクエストでは同じヘッダー挙動になりませんでした。
CWE-200 and CWE-524- GitHub Security Advisoryは、機密情報の漏えいにCWE-200、機密情報を含むキャッシュの使用にCWE-524を割り当てています。情報漏えいとキャッシュ境界を表し、CVEは割り当てられていません。
RubyGems package-flood attribution- RubyGemsは2026年5月の不正なパッケージ公開を確認しましたが、AIエージェントが作成・公開したかは判断できないとしています。 このキャンペーンとキャッシュ不備は別件です。キャンペーンがキー漏えいを引き起こしたという根拠はありません。
フレームワーク対応
インシデントのタイムライン
RubyGems 3.2.0で通常のgem signinがスコープ付きキーの作成に切り替わりましたが、サーバーは旧GETエンドポイントを維持しました。
Truffle Securityがキャッシュ不備をRubyGemsに報告しました。
RubyGemsがレスポンスヘッダーを修正し、Fastly上の対象キャッシュを削除しました。
RubyGemsがGHSA-9j48-x3c3-mrp2を公開し、旧キーをすべて失効させたと発表しました。GHSAの基本評価は7.3 High、RubyGemsの環境全体の評価は7.2 Highです。
RubyGemsは5月のパッケージ公開キャンペーンを別途説明し、AIエージェントによるものとは断定できないとしました。
ステップ・バイ・ステップの攻撃フロー
旧エンドポイントが認証情報を発行
/api/v1/api_keyへの認証済みGETは、広い権限を持つ旧APIキーを作成して返します。RubyGemsによると、gem 3.2.0未満のクライアントがこの経路を使用していました。新しいクライアントがスコープ付きキーへ移行した後も、サーバー側のエンドポイントは残っていました。
gzipがヘッダー処理を変える
クライアントがgzipを要求します。RubyGemsはRack::DeflaterとRack::ETagの連携を原因として挙げています。圧縮応答には単純なCache-Control: no-cacheだけが残り、privateとVary: Authorizationがありませんでした。
CDNが認証済み応答を再利用
対象のFastly設定では、応答がPOPに最大1時間保存される可能性がありました。キャッシュキーはAuthorizationごとに呼び出し元を分離していなかったため、エッジがRubyGemsに戻って次の利用者を認証せず、前の応答を返すことがありました。
別の利用者にキーが渡る
同じPOPに到達し同じキャッシュ条件になるリクエストには、前の利用者のキーが返る可能性がありました。RubyGemsはログイン間で偶発的に起きる場合に加え、未認証でキャッシュ応答を取得する可能性も説明しています。利用可能なログには悪用の証拠はありませんが、対象期間は限られています。
キーの権限が影響範囲を決める
漏えいした旧キーは、新しいgem版の公開、版の取り下げ、所有者変更、WebhookやTrusted Publisher設定の変更に使えました。公開済みリリースは書き換えられません。RubyGemsによると、UIとAPIの両方でMFAを要求すれば、キーだけによる公開・取り下げ・所有者変更を防げます。
修正で共有再利用を防ぎ、キーを失効
パッチはCache-Control: private, no-store、Surrogate-Control: max-age=0、Vary: Authorizationを追加します。RubyGemsはFastlyのキャッシュを削除し、旧キーを失効させ、GETログイン経路を廃止しました。スコープ付きキー、API MFA、CIでのTrusted Publishingを推奨しています。
ソースコード比較:脆弱 vs 堅牢化
未修正コントローラーの抜粋は、RubyGemsの公開修正で削除された行から再構成しています。問題のキャッシュヘッダーは、このアクションが公開キャッシュ方針を設定したためではなく、Rackミドルウェアの連携から生じました。修正後のcallbackとヘッダー値はcommit d3d11c0に一致します。private, no-storeは共有保存を防ぎ、Surrogate-Controlは中継キャッシュを無効化し、Vary: Authorizationは保存される場合も認証情報別に分離します。これはソース分析であり、リクエスト手順や単独適用できるパッチではありません。ベンダーの完全な修正を適用し、既存キャッシュを削除してください。
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
# 修正前のコントローラーには 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
# 修正で Api::BaseController に追加された。
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
エンジニアリング&システム堅牢化チェックリスト
- ✓RubyGems.orgの旧認証情報を使っていた場合は置き換えてください。RubyGemsは旧キーをすべて失効させました。gem版、予期しない取り下げ、所有者、Trusted Publisher、Webhookを監査し、既存リリースが書き換えられたとは考えないでください。
- ✓必要なgemと操作だけに制限したAPIキーを使い、UIとAPIの両方でMFAを有効にし、CIの長期シークレットよりTrusted Publishing/OIDCを優先してください。各workflowが意図したgemだけを公開できることを確認します。
- ✓CDN配下の認証済み応答や秘密情報には、オリジンでCache-Control: private, no-storeを設定し、サロゲートキャッシュを無効にします。認証の各要素をVary/キャッシュキーに含め、実際のCDN経路で圧縮の有無を両方試験してください。
- ✓Accept-Encoding: gzipを付けて認証情報エンドポイントを呼ぶ統合テストを追加し、private, no-store、Surrogate-Control: max-
age=0、Vary: Authorizationを確認します。2つ目の認証主体が共有エッジキャッシュから1つ目の応答を受け取らないことも検証してください。 - ✓キャッシュ漏えいを修正するときは、新しい秘密情報を発行する前に保存済みオブジェクトを削除します。過去ログは範囲の限られた証拠であり、漏えいがなかった証明ではありません。調査可能な期間を記録してください。
- ✓帰属を正確に記述してください。RubyGemsは2026年5月のキャンペーンでAIが作成したとは確認しておらず、APIキー窃取の成功も確認していません。
参考資料
- 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