Um pacote malicioso não precisa de um exploit engenhoso. Precisa de ser instalado uma vez, num portátil ou num build runner, por alguém que confia no registry. A partir daí pode ler variáveis de ambiente, tokens e chaves SSH, e enviá-los para outro sítio.
Há apenas meia dúzia de formas de entrar. Conhecê-las torna as defesas óbvias.
A cadeia
Quase todos os casos seguem a mesma forma, seja qual for o ecossistema:
- PublicarUm pacote é publicado, ou um existente recebe uma nova versão, com um payload lá dentro.
- Ser escolhidoUm programador, uma atualização do lockfile ou um resolver apanha-o.
- Correr na instalaçãoUm lifecycle script ou ficheiro de setup é executado durante a instalação, com as permissões do utilizador.
- RecolherO payload recolhe tokens, credenciais de cloud e variáveis de ambiente.
- PropagarTokens de publicação roubados são usados para publicar versões infetadas de outros pacotes.
O último passo não é hipotético. O worm npm Shai-Hulud, em setembro de 2025, fez exatamente isto, usando tokens roubados para republicar outros pacotes mantidos pelas suas vítimas.
Porta um: typosquats
O atacante publica um nome parecido com um popular: uma letra trocada, um hífen em falta, um sufixo plausível como -utils ou -cli. Muitas vezes o pacote funciona, porque embrulha ou reexporta o verdadeiro, por isso nada parte e ninguém olha duas vezes.
Como se apresenta: uma dependência que não reconhece num diff, com poucos downloads, uma primeira publicação recente e uma descrição copiada da original. Verifique as novas dependências diretas quando são adicionadas, não meses depois.
Os typosquats chegam cada vez mais por sugestões e não por erros de escrita. Um snippet copiado de um fórum, um tutorial desatualizado ou um assistente de código podem todos indicar um pacote que parece certo mas não existe, e os atacantes estão atentos a esses nomes e registam-nos. O hábito que ajuda é o mesmo em qualquer caso: antes de acrescentar uma dependência, abra a sua página no registry e o seu repositório de código-fonte e verifique que os dois coincidem e que o projeto tem histórico.
Porta dois: install scripts
O npm corre scripts preinstall, install e postinstall por defeito. As source distributions de Python podem correr código arbitrário em setup.py. Isso significa que o payload é executado antes de qualquer teste, linter ou code review o ver.
{
"name": "left-padder",
"version": "1.0.3",
"scripts": {
"postinstall": "node ./lib/telemetry.js"
}
}
Um script chamado telemetry.js ou setup.js que está minificado, codificado ou vai buscar uma segunda fase à rede é o sinal clássico. Pode listar os pacotes instalados que declaram um script postinstall:
npm query ":attr(scripts, [postinstall])" | jq -r '.[].name'
A maioria dos projetos tem uma lista curta de pacotes que precisam genuinamente de compilar algo nativamente. Tudo o resto nessa lista merece um olhar.
Porta três: tomada de controlo do maintainer
Este é o mais difícil de detetar, porque o nome do pacote está certo. Um atacante faz phishing a um maintainer, reutiliza um token exposto ou recebe a propriedade de um projeto abandonado e depois publica uma nova versão patch. O seu intervalo semver aceita-a automaticamente.
Como se apresenta: um patch release de um pacote parado há muito tempo, um novo publisher, um script de instalação que antes não existia ou uma versão sem tag correspondente no repositório de origem. As provenance attestations, onde o ecossistema as suporta, ajudam aqui: uma versão construída fora do pipeline normal do projeto não terá uma.
Porta quatro: dependency confusion
A sua organização tem um pacote interno, por exemplo acme-billing-utils, num registry privado. Um atacante publica o mesmo nome no registry público com um número de versão mais alto. Se as suas ferramentas olham para os dois registries e escolhem a versão mais alta, instalam a dele.
A má configuração clássica em Python é --extra-index-url, que junta índices em vez de dar preferência a um:
# risky: pip may resolve from either index
pip install --extra-index-url https://pypi.internal.example/simple acme-billing-utils
# safer: one index, which proxies the public one
pip install --index-url https://pypi.internal.example/simple acme-billing-utils
No npm, coloque os pacotes internos sob um scope e associe esse scope ao seu registry, para que o registry público nunca seja consultado:
# .npmrc
@acme:registry=https://npm.internal.example/
Verifique o seu em 10 minutos
- Corra o
npm queryacima no seu maior repositório e leia a lista. - Procure
extra-index-urlna config de CI e na documentação para programadores. - Liste os nomes de pacotes internos e confirme que cada um tem scope ou está reservado no registry público.
- Verifique que tokens de publicação existem para os seus próprios pacotes, quem os tem e se têm scope limitado e curta duração.
- Veja os diffs do lockfile do último mês: as novas dependências diretas foram revistas por uma pessoa?
Nenhum destes precisa de um produto. Precisam de alguém que olhe, uma vez, e depois de um gate para não ter de voltar a olhar à mão.