Uma API Express chamada api-service pode passar nos testes, gerar uma imagem OCI e, ainda assim, incluir código que ninguém pretendia aprovar. Um desenvolvedor adiciona expresss em vez de express ao package.json; o registro público resolve o pacote com nome semelhante, o executor do GitHub Actions o instala e a imagem de lançamento o contém. O atacante não precisou de acesso de escrita ao repositório, de uma chave de implantação roubada ou de acesso ao cluster de produção. A pergunta para quem defende o sistema é mais específica e útil: como impedimos que código não revisado entre em um build por meio da resolução de dependências? Acompanharemos o api-service desde o seu manifesto, passando pelo build de CI, até a sua imagem publicada.
O fluxo de build atravessa várias fronteiras de confiança antes que a imagem chegue à implantação. Um registro público de pacotes fornece código com base em um nome presente no manifesto, o executor de CI realiza a instalação e o registro de artefatos armazena a imagem resultante para implantação posterior. Um pacote malicioso pode tentar executar código durante o build, introduzir uma dependência transitiva nociva ou roubar um token de publicação disponível para o executor. O fluxo a seguir torna essas fronteiras visíveis:
Este diagrama mostra por que a seleção de dependências é uma decisão relacionada à cadeia de suprimentos, e não apenas um detalhe local de codificação. Assim que o executor (runner) busca um pacote, seu conteúdo passa a fazer parte da entrada do processo de build e pode executar hooks de instalação, a menos que o pipeline restrinja essas ações.
[fontes][/fontes]
O arquivo de manifesto é a primeira barreira, pois o comando npm install trata cada nome de pacote declarado como uma instrução para resolver e buscar código. No api-service, espera-se o pacote express na versão 4.18.3, enquanto o nome inventado expresss na versão 1.0.0 é um erro de digitação que aponta para um pacote diferente. Esse ataque é conhecido como typosquatting (ou squatting por erro de digitação): um atacante publica um pacote com nome semelhante ao de uma dependência confiável e conta com o erro humano para que ele seja selecionado.
[fontes][/fontes]
Um arquivo de trava (lockfile) registra a versão resolvida do pacote, os metadados do registro e o hash de integridade para um determinado grafo de dependências. Esse registro torna as instalações subsequentes reproduzíveis apenas depois que o pacote incorreto já tiver sido aceito e enviado ao repositório (commit). Portanto, a falha na revisão fica visível em dois locais: o package.json contém uma dependência direta inesperada, e o package-lock.json contém uma nova entrada resolvida e um valor de integridade. Quais nome de pacote e entrada no lockfile deveriam fazer com que a adição de expresss fosse reprovada na revisão?
[fontes][/fontes]
Este código demonstra que uma instalação bem-sucedida não é prova de que a dependência pretendida foi selecionada. O instalador resolveu expresss exatamente como solicitado e registrou o resultado no arquivo de bloqueio (lockfile); a repetição determinística desse erro continua sendo um erro. Revisar as diferenças (diffs) nas dependências significa tratar um nome desconhecido, uma versão inesperada ou uma entrada de integridade recém-adicionada como código que está sendo incorporado à versão de lançamento.
Regra de revisão Um arquivo de bloqueio comprova o que o instalador selecionou. Ele não comprova que a seleção foi autorizada ou segura.
Um nome público grafado incorretamente tem origem no manifesto, mas um pacote privado pode ser substituído mesmo quando seu nome está correto. A confusão de dependência (dependency confusion) ocorre quando um processo de build resolve um pacote a partir de um registro não pretendido, geralmente porque um pacote público com o mesmo nome — ou uma versão compatível mais recente — prevalece sobre um pacote interno. O atacante precisa ter a capacidade de publicar em um registro consultado pelo processo de build, e não acesso ao repositório de código-fonte privado.
Para o api-service, uma dependência interna como @acme/auth-client deve sempre ser resolvida a partir do registro privado da organização. Um intervalo de versões amplo permite o uso de versões futuras que satisfaçam esse intervalo, enquanto a precedência dos registros determina onde o instalador realizará a busca. O arquivo de bloqueio fixa o resultado para um build consolidado, mas não consegue corrigir um mapeamento de registro que direcione uma nova compilação para a fonte incorreta, nem proteger contra uma atualização deliberada do arquivo de bloqueio que não seja inspecionada pelos revisores. O objetivo do controle é garantir uma resolução determinística a partir do registro pretendido para cada escopo de pacote.
A comparação distingue o limite que cada ameaça ultrapassa das evidências que a revelam: