flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems: утечка API-ключа через CDN-кэш
Сбой gzip/CDN раскрыл старые ключи RubyGems. Здесь описаны исправление и восстановление; связь отдельной кампании с ИИ-агентами оспаривается.
Представьте стойку, где каждому проверенному клиенту выдают новый мастер-ключ, а копию кладут в общую корзину: упаковочная машина изменила маркировку конверта. Следующий посетитель той же стойки мог получить ключ предыдущего клиента без проверки удостоверения. Исправление запрещает общим кэшам хранить такие приватные ответы и разделяет ответы по данным авторизации.
Ключевые понятия и термины
Legacy API key- Долговременный ключ RubyGems с широкими правами управления gem-пакетами. По бюллетеню, он позволял публиковать новую версию, снимать версии, добавлять владельцев и менять вебхуки или настройки доверенной публикации; войти в учетную запись или переписать уже опубликованный релиз с его помощью было нельзя.
Shared cache / POP- Точка присутствия CDN сохраняет ответ для повторного использования. Если аутентифицированный ответ с секретом попадает в общий кэш без приватной политики и ключа кэша, учитывающего авторизацию, другой пользователь может получить этот ответ.
Compression middleware- RubyGems сообщает, что Accept-Encoding: gzip запускал взаимодействие middleware Rack, из-за которого ответ оставался без директивы private и без Vary: Authorization. Обычные запросы без gzip не давали тех же заголовков.
CWE-200 and CWE-524- В GitHub Security Advisory указаны
CWE-200— раскрытие чувствительной информации иCWE-524— использование кэша с чувствительной информацией. Они описывают раскрытие и границу кэширования; CVE не присваивался. RubyGems package-flood attribution- RubyGems подтвердила злоупотребление публикацией пакетов в мае 2026 года, но не смогла установить, создавали или публиковали ли их ИИ-агенты. Не смешивайте кампанию с ошибкой кэша: источники не подтверждают, что кампания привела к утечке ключей.
Связи со стандартами
Хронология инцидента
В 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 отдельно описала майскую кампанию публикации пакетов и заявила, что не может приписать ее ИИ-агентам.
Пошаговый сценарий атаки
Старый маршрут выдает учетные данные
Аутентифицированный GET-запрос к /api/v1/api_key создает и возвращает старый API-ключ с широкими правами. RubyGems указывает, что клиенты gem до версии 3.2.0 использовали этот маршрут; сервер сохранил его и после перехода новых клиентов на ключи с ограниченными правами.
Gzip меняет путь формирования заголовков
Клиент запрашивает gzip. RubyGems связывает кэширование с взаимодействием Rack::Deflater и Rack::ETag: сжатый ответ получал только простой Cache-Control: no-cache, без private и без Vary: Authorization.
CDN повторно использует аутентифицированный ответ
В затронутой конфигурации Fastly ответ мог храниться в точке присутствия до часа. Ключ кэша не разделял клиентов по Authorization, поэтому edge-узел мог отдать предыдущий ответ, не обращаясь к RubyGems для проверки следующего клиента.
Другой клиент получает ключ
Запрос с тем же вариантом кэша, прошедший через ту же точку присутствия, мог получить ключ предыдущего пользователя. По словам RubyGems, это могло произойти случайно между входами; компания также описывает неаутентифицированное получение ответа из кэша. Доступные журналы не выявили злонамеренного использования, но охватывали лишь ограниченный период.
Права ключа ограничивают последствия
Утекший старый ключ мог разрешить новые версии gem, снятие версий, смену владельцев и изменение вебхуков или доверенных издателей. Уже опубликованные релизы нельзя было переписать. RubyGems сообщает, что MFA для действий и в интерфейсе, и в API блокирует операции публикации, снятия и смены владельца при наличии только ключа.
Исправление исключает общее кэширование и отзывает ключи
Патч добавляет Cache-Control: private, no-store, Surrogate-Control: max-age=0 и Vary: Authorization. RubyGems очистила кэш Fastly, отозвала старые ключи и отключила GET-вход. Рекомендации: ключи с ограниченными правами, MFA для API и доверенная публикация для CI.
Исходный код: Уязвимый vs Защищённый вариант
Фрагмент контроллера до исправления восстановлен по удаленным строкам публичного патча RubyGems; небезопасные заголовки кэша возникали из-за взаимодействия middleware Rack, а не из-за публичной политики кэширования в этом действии. Защитный callback и значения заголовков соответствуют коммиту d3d11c0. private, no-store запрещает общее хранение, Surrogate-Control отключает кэширование прокси, а Vary: Authorization разделяет ответы в кэше, если он все же сохраняет их. Это анализ исходного кода, а не инструкция по запросам и не автономный патч: применяйте полное исправление поставщика и очищайте старые записи кэша.
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
# В контроллере до исправления отсутствовал 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
# Добавлено в 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 отозвала все такие ключи. Проверьте версии, неожиданные снятия, владельцев, доверенных издателей и вебхуки; не считайте, что существующие релизы были переписаны.
- ✓Используйте API-ключи с минимальными правами для нужных gem и действий, включите MFA для интерфейса и API, а в CI отдавайте предпочтение доверенной публикации/OIDC вместо долгоживущих секретов. Проверьте, что каждый workflow публикует только требуемый gem.
- ✓Для аутентифицированных ответов или секретов за CDN задавайте на источнике Cache-Control: private, no-store, отключайте кэширование прокси и включайте все параметры авторизации в Vary/ключ кэша. Проверяйте реальный путь через CDN с компрессией и без нее.
- ✓Добавьте интеграционный тест для запроса к маршруту учетных данных с Accept-Encoding: gzip: он должен проверять private, no-store, Surrogate-Control: max-
age=0и Vary: Authorization. Также убедитесь, что вторая учетная запись не получает ответ первой через общий edge-кэш. - ✓При исправлении утечки из кэша очистите сохраненные объекты до выдачи новых секретов; считайте старые журналы ограниченным свидетельством, а не доказательством отсутствия утечки. Зафиксируйте период, который действительно можно проверить.
- ✓Точно указывайте атрибуцию: RubyGems не подтвердила авторство ИИ в майской кампании 2026 года и не обнаружила доказательств успешной кражи 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