flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems: CDN 캐시로 인한 API 키 노출
gzip/CDN 캐시 결함으로 RubyGems의 레거시 키가 노출됐습니다. 수정과 복구 방법을 설명하며 별도 캠페인의 AI 연루는 논쟁 중입니다.
신원을 확인한 고객마다 새 마스터 키를 만들어 주는 창구를 떠올려 보세요. 포장 장비가 봉투 라벨을 바꾸면서 사본을 공동 트레이에 놓았습니다. 같은 창구를 다음에 이용한 사람은 신분증을 보이지 않고도 앞선 고객의 키를 가져갈 수 있습니다. 수정 사항은 공유 캐시에 비공개 응답을 저장하지 못하게 하고, 권한 정보에 따라 응답을 분리합니다.
핵심 개념 및 용어
Legacy API key- 광범위한 gem 관리 권한을 가진 장기 유효 RubyGems 자격 증명입니다. 보안 공지에 따르면 새 gem 버전 게시, 버전 철회, 소유자 추가, webhook 또는 trusted publisher 설정 변경이 가능했습니다. 계정 로그인이나 이미 게시된 릴리스 수정은 불가능했습니다.
Shared cache / POP- CDN의 POP(Point of Presence)는 이후 요청에서 재사용할 응답을 저장합니다. 자격 증명이 든 인증 응답에 비공개 캐시 정책과 권한별 캐시 키가 없으면 다른 호출자가 그 응답을 받을 수 있습니다.
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에 따르면 3.2.0 이전 gem 클라이언트가 이 경로를 사용했습니다. 새 클라이언트가 범위 제한 키로 바뀐 뒤에도 서버는 기존 엔드포인트를 유지했습니다.
gzip이 헤더 처리 경로를 바꿈
클라이언트가 gzip을 요청합니다. RubyGems는 Rack::Deflater와 Rack::ETag의 상호작용을 캐시 동작의 원인으로 설명합니다. 압축 응답에는 단순한 Cache-Control: no-cache만 남았고 private와 Vary: Authorization이 없었습니다.
CDN이 인증된 응답을 재사용
영향받은 Fastly 구성에서는 응답이 POP에 최대 한 시간 저장될 수 있었습니다. 캐시 키가 Authorization별 호출자를 구분하지 않아, edge가 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 공개 수정에서 삭제된 문맥을 바탕으로 재구성했습니다. 안전하지 않은 캐시 헤더는 해당 action이 공개 캐시 정책을 설정해서가 아니라 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가 모든 레거시 키를 폐기했습니다. 버전, 예상하지 못한 철회, 소유자, trusted publisher, webhook을 감사하되 기존 릴리스가 다시 작성되었다고 여기지 마세요.
- ✓필요한 gem과 작업에만 권한이 있는 API 키를 사용하고 UI와 API 모두에 MFA를 설정하며, CI 장기 비밀 대신 trusted publishing/OIDC를 우선하세요. 각 workflow가 의도한 gem만 게시할 수 있는지 검증하세요.
- ✓CDN 뒤의 인증 응답이나 비밀 정보에는 원본 서버에서 Cache-Control: private, no-store를 설정하고 surrogate 캐시를 비활성화하며 권한 요소를 모두 Vary/캐시 키에 반영하세요. 실제 CDN 경로에서 압축 유무를 모두 테스트하세요.
- ✓Accept-Encoding: gzip으로 자격 증명 경로를 요청하는 통합 테스트를 추가하고 private, no-store, Surrogate-Control: max-
age=0, Vary: Authorization을 확인하세요. 두 번째 주체가 공유 edge 캐시에서 첫 번째 응답을 받을 수 없는지도 검증하세요. - ✓캐시 유출을 수정할 때 새 비밀을 발급하기 전에 저장된 객체를 삭제하세요. 과거 로그는 제한된 증거일 뿐 유출이 없었다는 증명이 아닙니다. 실제 조사 가능한 기간을 기록하세요.
- ✓귀속 표현을 정확히 하세요. 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