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 · Alta
Pesquisa · FlawOpen

RubyGems: vazamento de chave API por cache CDN

Falha gzip/CDN expôs chaves RubyGems. Veja correção e recuperação; a autoria da campanha de pacotes por IA não foi confirmada.

💡 Explicação em Linguagem Simples (ELI5)

Imagine um balcão que cria uma chave mestra nova para cada cliente verificado e deixa uma cópia numa bandeja compartilhada porque uma máquina de embalagem alterou o rótulo do envelope. A próxima pessoa atendida naquele mesmo balcão poderia receber a chave do cliente anterior sem mostrar documento. A correção impede caches compartilhados de guardar respostas privadas e separa as respostas conforme a autorização.

Conceitos Centrais e Termos

Legacy API key
Uma credencial RubyGems duradoura, com amplas permissões de gerenciamento de gems. O aviso diz que ela podia publicar uma nova versão, remover versões, adicionar responsáveis e alterar webhooks ou configurações de trusted publishing; não permitia entrar na conta nem reescrever uma versão já publicada.
Shared cache / POP
Um ponto de presença da CDN guarda uma resposta para reutilizá-la em outras requisições. Se uma resposta autenticada com credencial for compartilhada sem política privada e sem uma chave de cache que considere autorização, outra pessoa pode receber a resposta.
Compression middleware
A RubyGems informa que Accept-Encoding: gzip acionava uma interação do Rack que deixava a resposta sem a diretiva private e sem Vary: Authorization. Requisições comuns sem gzip não mostravam o mesmo comportamento de cabeçalho.
CWE-200 and CWE-524
O GitHub Security Advisory associa CWE-200 à exposição de informação sensível e CWE-524 ao uso de cache com informação sensível. Eles representam a divulgação e o limite do cache; não foi atribuído CVE.
RubyGems package-flood attribution
A RubyGems confirmou publicação abusiva de pacotes em maio de 2026, mas disse que não conseguiu determinar se agentes de IA os criaram ou publicaram. Separe essa campanha da falha de cache: as fontes não demonstram que ela causou a exposição de chaves.

Mapeamentos de frameworks

Linha do Tempo do Incidente

2020-12-10

O RubyGems 3.2.0 mudou o fluxo padrão de gem signin para criar chaves com escopo, mas o servidor manteve disponível a rota GET legada.

2026-07-06

A Truffle Security comunicou a falha de cache à RubyGems.

2026-07-09

A RubyGems implantou a correção dos cabeçalhos e limpou objetos afetados do cache Fastly.

2026-07-22

A RubyGems publicou GHSA-9j48-x3c3-mrp2 e informou que revogou todas as chaves legadas. O GHSA atribui 7,3 (High); a RubyGems informa pontuação ambiental geral 7,2 (High).

2026-09-11

A RubyGems descreveu separadamente a campanha de publicação de pacotes de maio e disse não poder atribuí-la a agentes de IA.

Fluxo de Ataque Passo a Paso

Passo 1

Uma rota legada emite uma credencial

Uma requisição autenticada GET para /api/v1/api_key cria e devolve uma chave legada com amplas permissões. A RubyGems diz que clientes gem anteriores à versão 3.2.0 usavam essa rota; o servidor a manteve mesmo depois que clientes novos migraram para chaves com escopo.

Passo 2

Gzip muda o caminho dos cabeçalhos

O cliente solicita gzip. A RubyGems atribui o comportamento de cache à interação entre Rack::Deflater e Rack::ETag: a resposta comprimida ficava apenas com Cache-Control: no-cache, sem diretiva private e sem Vary: Authorization.

Passo 3

A CDN reutiliza uma resposta autenticada

Na configuração afetada da Fastly, a resposta podia ficar armazenada num POP por até uma hora. A chave de cache não separava clientes pelo cabeçalho Authorization; assim, a borda podia devolver a resposta anterior sem consultar a RubyGems para autenticar o próximo cliente.

Passo 4

Outra pessoa recebe a chave

Uma requisição com a mesma variante de cache, encaminhada pelo mesmo POP, podia receber a chave do usuário anterior. A RubyGems diz que isso podia ocorrer acidentalmente entre logins e também descreve consulta não autenticada de uma resposta em cache. Os registros disponíveis não mostraram uso malicioso, mas cobriam apenas um período limitado.

Passo 5

O escopo da chave limita as consequências

Uma chave legada vazada podia autorizar novas versões de gems, remoções, mudanças de responsáveis e alterações em webhooks ou trusted publishers. Versões já publicadas não podiam ser reescritas. Segundo a RubyGems, MFA exigida tanto na interface quanto na API bloqueia ações de publicação, remoção e alteração de responsáveis feitas só com a chave.

Passo 6

A correção impede o compartilhamento e aposenta as chaves expostas

O patch adiciona Cache-Control: private, no-store, Surrogate-Control: max-age=0 e Vary: Authorization. A RubyGems limpou o cache da Fastly, revogou chaves legadas e aposentou a rota GET de login. Recomenda chaves com escopo, MFA na API e trusted publishing para CI.

Código-Fonte: Vulnerável vs. Seguro

O trecho vulnerável do controller foi reconstruído a partir das linhas removidas no patch público da RubyGems; os cabeçalhos de cache inseguros vinham da interação do middleware Rack, não de uma política pública configurada nessa ação. O callback endurecido e os cabeçalhos correspondem ao commit d3d11c0. private, no-store impede armazenamento compartilhado, Surrogate-Control desativa cache de proxy e Vary: Authorization separa qualquer cache que ainda armazene uma resposta. É análise de código-fonte, não um roteiro de requisição nem um patch isolado; aplique a correção completa do fornecedor e limpe entradas antigas.

IMPLEMENTAÇÃO VULNERÁVEL
class Api::V1::ApiKeysController < Api::BaseController
  include ApiKeyable

  # O controlador antes da correção não tinha o 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
PATCH SEGURO E ROBUSTO
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

# Adicionado a Api::BaseController pela correção.
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

Lista de Verificação de Segurança para Engenharia

Fontes