Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

Ataque à Supply Chain no PyPI: a lição do LiteLLM

O comprometimento do LiteLLM no PyPI levou um roubador de credenciais a todos os wheels. Veja o que hash pinning e lockfiles impediriam e o que não.

Uma esteira com caixas lacradas, em que uma caixa adulterada vaza fios em direção às outras.
Um único pacote comprometido na esteira compromete silenciosamente tudo o que vem depois dele.

Três arquivos. Essa é a pegada inteira de um dos ataques de supply chain em Python mais perigosos que já vimos: um cook.pth de 12 bytes, um cook.py de 22 KB e um sitecustomize.py de 25 KB. Quando o LiteLLM 1.107.0 foi publicado no PyPI, esses três arquivos vieram junto em cada um dos seus sete wheels e, por causa do funcionamento da inicialização do Python, eram executados em todo processo Python de um host infectado: sua aplicação, seu runner de CI, o próprio pip, até o language server do seu editor. Você não precisava dar import em litellm. Não precisava executá-lo. Bastava iniciar qualquer interpretador Python.

A equipe do LiteLLM confirmou o comprometimento e o PyPI removeu a versão em poucas horas, mas a janela foi longa o bastante. As imagens Docker maliciosas, incluindo a tag latest, ficaram disponíveis por cerca de duas horas e meia. Se seu CI puxou algo nesse intervalo, o roubador já tinha coletado chaves SSH, credenciais de nuvem, senhas de navegador, segredos de CI/CD e tokens de ferramentas de IA, criptografado tudo e enviado para um domínio controlado pelo atacante. Não foi um typosquat nem um pacote parecido. Era o pacote de verdade, construído pelo pipeline de verdade, assinado pelo processo de release de verdade. E isso deveria mudar a forma como você pensa a confiança em dependências.

Como três arquivos sequestraram o pipeline oficial de release

O atacante não invadiu o PyPI e também não enfiou um commit no repositório público, já que a fonte upstream no GitHub está limpa. Segundo o relatório de incidente do LiteLLM, o atacante tinha as credenciais da GitHub CLI de três desenvolvedores, aparentemente extraídas dos logs de uma instância interna do LiteLLM exposta à internet. Com o token de um owner da organização, ele contornou a proteção de branch e apontou o workflow privado de release para um repositório parecido, Berri-AI/litellm em vez de BerriAI/litellm. A credencial de um segundo desenvolvedor satisfez o gate de aprovação. O pipeline oficial então construiu, assinou e publicou os artefatos do atacante.

A injeção em si era elegantemente pequena. Uma linha extra no pyproject.toml do sdist:

[tool.maturin]
python-source = "."
data = "cook"            # <-- injected, copies cook/ into the wheel
python-packages = ["litellm_cli"]   # legitimate, present upstream

Essa diretiva data = "cook" instrui o maturin a empacotar um diretório cook/ dentro do caminho purelib do wheel, e o pip o instala dutifamente no site-packages. O sdist em si não tinha a pasta, então quem revisasse o release veria uma única linha enigmática e nada mais. A carga maliciosa se escondia nos artefatos de build, não no código-fonte. Revisar o histórico do git, que a maioria de nós trata como fonte da verdade, não mostraria absolutamente nada.

Por que arquivos .pth e sitecustomize.py são um sonho para malware

Duas funcionalidades da sequência de inicialização do Python tornaram esse ataque incomumente potente e, sinceramente, são recursos que a maioria dos desenvolvedores Python nunca parou para pensar. Primeiro: qualquer arquivo .pth no site-packages é processado na inicialização do interpretador, e linhas que começam com import são executadas. O cook.pth de 12 bytes tinha exatamente uma linha, import cook, e isso bastou para rodar o roubador em todo processo Python da máquina. Segundo: o Python importa automaticamente um módulo chamado sitecustomize na inicialização, se encontrar um no caminho. Os atacantes colocaram o próprio, que criava um daemon destacado que fazia contato com um arquivo de texto hospedado no GitHub a cada hora, baixava qualquer código que o atacante apontasse e o executava com exec().

Isso é bem mais grave do que um pacote malicioso comum com um setup.py envenenado ou um import ruim em __init__.py. Esses executam quando você instala ou importa o pacote. Este executava quando você rodava qualquer coisa em Python. Seu pytest no CI. O pip list que você digitou para ver o que aconteceu. O formatador black no seu IDE. Cada invocação rearmava a armadilha.

Vale parar para pensar no que o roubador de fato coletava, porque a lista parece um mapa da vida moderna de quem desenvolve software: credenciais de AWS, Azure e gcloud, kubeconfigs, tokens da GitHub CLI, chaves SSH e GPG, cofres de senhas de navegador, históricos de shell, segredos de CI dos runners do GitLab e do Jenkins, credenciais do Terraform, variáveis de ambiente de outros processos lidas via /proc/<pid>/environ, credenciais do serviço de metadados de instâncias AWS e, com uma ironia sombria, tokens do Claude Code, Cursor, Copilot e dezenas de outras ferramentas de programação com IA, além das chaves de provedores de LLM armazenadas no próprio banco do proxy do LiteLLM. Um único pip install e o atacante ficava com efetivamente toda a sua identidade como engenheiro.

Você não precisava dar import em litellm. Iniciar qualquer interpretador Python no host bastava para executar o roubador.

Hash pinning e lockfiles: necessários, mas não suficientes

Todo checklist de segurança de supply chain começa por lockfiles e hash pinning, e com razão. Se você fixa hashes com pip install --require-hashes ou usa uma ferramenta como uv ou Poetry com hashes travados, um wheel que muda silenciosamente depois de você tê-lo validado é rejeitado. Isso é proteção real contra toda uma classe de ataques: adulteração de registry, envenenamento de mirror, substituição de artefatos.

Mas sejamos honestos sobre o que isso faz aqui: nada, pelo menos não para quem atualizou primeiro. Hash pinning garante que o artefato que você instala amanhã é byte a byte idêntico ao que você instalou hoje. Nesse ataque, os wheels maliciosos eram os artefatos oficiais, de origem própria, construídos pelo pipeline, com hashes consistentes. No momento em que alguém rodou a atualização de rotina, resolveu a nova versão e commitou o lockfile atualizado, os hashes maliciosos foram fixados no repositório com um check verde de CI. Pinning protege você contra mudanças; não diz nada sobre intenção.

Onde o pinning realmente brilha em uma história como essa é no raio de impacto. Times com hashes fixados e uma cadência deliberada de upgrades não absorvem automaticamente cada release na hora em que ela sai. Se sua política é “atualizar dependências em um PR semanal, com um período de espera de alguns dias para qualquer coisa que não seja uma correção crítica”, a remoção do PyPI às 13:23 UTC aconteceu antes de a sua janela de atualização sequer abrir. Processo chato vence ferramenta esperta com mais frequência do que gostaríamos de admitir.

Então mantenha seus lockfiles. Gere-os com hashes. Mas pare de dizer a si mesmo que eles são uma defesa contra um pipeline de mantenedor comprometido. Eles são uma defesa contra drift e adulteração, e um quebra-molas contra releases ruins. Só isso.

Uma porta de cofre robusta ao lado de uma parede com um buraco escancarado.
O hash pinning tranca a porta da frente; esse ataque atravessou a parede, que era o próprio pipeline de build.

A atestação de proveniência teria pego este caso

Aqui está o lado desconfortável: esse ataque específico tinha uma anomalia gritante e detectável, e as ferramentas do ecossistema para pegá-la já existem hoje. Os wheels maliciosos não foram construídos a partir do repositório público. Foram construídos a partir de um repositório parecido criado pelo atacante. Qualquer etapa de verificação que pergunte “este artefato foi construído a partir do commit e do repositório que ele declara?” teria falhado de forma ruidosa.

É exatamente isso que a atestação de proveniência oferece. O PyPI suporta atestações PEP 740: quando um pacote é publicado via Trusted Publishers a partir de GitHub Actions, o workflow pode gerar uma declaração assinada que vincula o artefato ao repositório, workflow e commit exatos que o produziram. Uma verificação do lado do consumidor se torna mecânica: verifique a atestação e confirme que o repositório de origem é BerriAI/litellm, e não Berri-AI/litellm. Bundles do Sigstore, proveniência SLSA e verificações no estilo pip-audit pertencem todos a essa família.

O gap hoje é de adoção e de enforcement. As atestações são opcionais para quem publica, e quase ninguém as verifica na instalação, mesmo quando existem. É essa parte que precisa mudar, e ela combina com um ponto que já discutimos antes sobre infraestrutura de confiança que só funciona quando alguém realmente a verifica: uma assinatura que ninguém verifica é só decoração. Se sua organização mantém o próprio índice ou proxy de pacotes, e se você tem mais que um punhado de engenheiros deveria manter, “rejeitar artefatos sem proveniência verificável da origem esperada” é uma política que você pode aplicar de forma centralizada, uma vez, para todos os desenvolvedores.

Há também uma lição mais sutil. O atacante precisou de três credenciais de desenvolvedor: uma para redirecionar o workflow, uma para aprovar e uma para não levantar suspeitas. Isso é uma falha sistêmica de higiene de credenciais, não um erro isolado. Tokens de Trusted Publisher de curta duração, baseados em OIDC, no PyPI teriam reduzido drasticamente a superfície de ataque, já que não há token de longa duração parado em um arquivo de log para ser roubado. Se o seu projeto ainda publica com tokens de API guardados como segredos de CI, considere isto um toque.

Instalações em sandbox e o caso para períodos de espera

Vamos supor o pior: um release malicioso escapa da sua higiene de lockfiles e das suas verificações de proveniência, porque um dia alguma coisa vai escapar. O que sobra? Contenção. A pergunta deixa de ser “como impedimos pacotes ruins?” e passa a ser “quando um pacote ruim roda, o que ele consegue alcançar?”

Para o CI, a resposta é genuinamente alcançável. Builds deveriam rodar em ambientes efêmeros, sem credenciais de longa duração, com tokens de escopo limitado e filtragem de egress. O comportamento do roubador do LiteLLM é barulhento na rede, se você estiver escutando: um POST para um domínio desconhecido com um header de autenticação customizado, um fetch de uma URL raw do GitHub em loop a cada hora e, o sinal mais evidente de todos, um processo Python consultando 169.254.169.254, o serviço de metadados de instância da AWS. Código de aplicação quase nunca tem motivo legítimo para acessar o IMDS diretamente, porque o SDK cuida disso. Alerte sobre processos Python que acessam esse endpoint e você pegará toda uma família de ladrões de credenciais, não só este. Já escrevemos sobre os pontos cegos no log de autenticação em nuvem; este é um deles, e é barato de fechar.

Máquinas de desenvolvedores são mais difíceis, e não vou fingir o contrário. Um laptop onde o mesmo usuário roda o editor, o navegador, o agente SSH e pip installs arbitrários é um banquete para um roubador. As medidas pragmáticas: faça instalações exploratórias em containers ou devcontainers em vez do ambiente do host; mantenha as instalações de pacotes dentro do devcontainer do projeto para que um sitecustomize.py malicioso nunca caia no site-packages do seu host; e trate o ~/.aws/credentials como um passivo. SSO com tokens de curta duração (aws sso login, gcloud auth) significa que há bem menos coisa guardada em disco para roubar. Nada disso é exótico. É só atrito, e o atrito é justamente o objetivo.

  • Fixe versões e hashes em um lockfile, mas trate upgrades como eventos deliberados, não como ruído de fundo.
  • Imponha uma janela de espera de 48 a 72 horas antes que upgrades de dependências não críticas cheguem a builds de produção. A maioria dos releases maliciosos é removida em poucas horas.
  • Verifique atestações de proveniência onde elas existirem e prefira pacotes publicados via Trusted Publishers com OIDC em vez de tokens de API de longa duração.
  • Rode builds de CI em ambientes efêmeros, sem credenciais, com filtragem de egress; alerte sobre qualquer processo Python que acesse endpoints de metadados de instância.
  • Instale e execute código de terceiros em containers nas máquinas dos desenvolvedores, e substitua arquivos estáticos de credenciais de nuvem por sessões SSO de curta duração.
  • Audite seu ambiente em busca dos IoCs se você puxou algo na janela: cook.pth, cook.py ou um sitecustomize.py perdido no site-packages, além de /tmp/.pc_once_* ou /tmp/remote_code_loader.lock no disco.

O que esse ataque revela sobre onde está o risco real

Repare no que o atacante não se deu ao trabalho de fazer. Ele não explorou uma vulnerabilidade de código. Não precisou de um zero-day. Foi atrás do alvo mais frágil do software moderno: as pessoas e as máquinas que guardam credenciais permanentes. Três tokens da GitHub CLI, aparentemente extraídos dos logs de uma instância interna exposta à internet, bastaram para assumir um pipeline de release que serve milhões de downloads. É o mesmo padrão que vemos se repetir na indústria: segredos espalhados por logs, agentes e arquivos de configuração, cada um deles uma chave mestra. Se você roda uma instância interna de qualquer coisa e a expõe à internet, os logs dela agora fazem parte do seu modelo de ameaças. Essa frase deveria deixar algumas pessoas desconfortáveis.

Há também um problema de velocidade que vale nomear. O LiteLLM é um projeto de evolução rápida, com dezenas de milhares de commits e releases frequentes, e está no centro da stack de ferramentas de IA, puxado por frameworks e SDKs que muitos times nem sabem que dependem dele. O alcance transitivo significa que você não escolhe sua exposição escolhendo suas dependências diretas. Um SBOM deixa de ser papelada de compliance nesse cenário; é como você responde “somos afetados?” em minutos, e não em dias. Ferramentas que geram uma lista de materiais a partir de evidências reais de build, e não raspando manifestos, vão pegar as dependências que o seu requirements.txt não menciona.

Uma figura solitária com uma lanterna inspecionando uma caixa entre prateleiras de armazém sem fim.
Árvores de dependências modernas são armazéns; a maioria dos times nunca inventariou o que está nas prateleiras.

Minha opinião: a resposta da indústria a ataques de supply chain tem se concentrado demais em teatro de prevenção, com scanners que apontam versões conhecidamente ruins depois do fato, e de menos nos controles pouco glamourosos que mudariam essa história. Credenciais de publicação de curta duração. Verificação de proveniência no proxy. Períodos de espera. Alertas de egress para endpoints de metadados. Instalações em sandbox. Nenhuma dessas é um problema de pesquisa. São decisões de configuração, disponíveis hoje, e a maioria dos times não implementou nenhuma delas. Os atacantes do LiteLLM precisaram de uma linha ruim em uma configuração de build e três tokens roubados. As defesas acima os obrigariam a vencer quatro camadas independentes em vez de uma. Isso é o que defesa em profundidade significa de fato: não mais scanners, mas mais formas independentes de falhar. É a mesma filosofia por trás de engenharia de sistemas que resistem a catástrofes, em vez de sistemas que apenas torcem para dar certo.

O próximo release comprometido já está sendo planejado por alguém. Você não vai impedir que ele seja publicado. Mas pode decidir se ele roda com suas chaves da AWS ao alcance, seu agente SSH destravado e um caminho de rede livre até um domínio controlado pelo atacante, ou se ele morre num container efêmero, sem credenciais e atrás de um firewall. Essa decisão é tomada agora, na sua configuração de CI e no seu setup de devcontainer, e não no dia em que o relatório de incidente for publicado.