Faça uma conta desconfortável. Rode du -sh node_modules e compare com o tamanho do seu src.
A proporção costuma ser absurda. O código que você escreveu, revisou e testou é uma fração minúscula do que roda em produção. O resto veio de estranhos, atualiza sozinho e tem acesso a tudo que o seu processo tem.
Não é paranoia teórica: os casos conhecidos de pacote popular comprometido, mantenedor que transfere a conta pra alguém mal-intencionado e biblioteca sabotada pelo próprio autor já são muitos.
As três formas de ataque
Pacote comprometido. O mantenedor perde a conta, ou entrega ela pra alguém, e uma versão nova sai com código malicioso. Como o pacote é legítimo e amplamente usado, ele entra em milhares de projetos numa tarde.
Nome parecido. lodahs em vez de lodash, crossenv em vez de cross-env. Instala-se por erro de digitação ou por copiar de um tutorial ruim.
Confusão de dependência. Você tem um pacote interno chamado @empresa/utils. Alguém publica um pacote público com o mesmo nome e versão mais alta. Dependendo da configuração do seu gerenciador, o público ganha.
Esse terceiro é o mais elegante e o mais esquecido. A defesa é configurar escopo explícito por registro:
# .npmrc
@empresa:registry=https://registro-interno.exemplo.com/
//registro-interno.exemplo.com/:_authToken=${NPM_TOKEN}
O que realmente reduz risco
Em ordem de retorno por esforço.
Lockfile commitado e instalação estrita. Sem isso, nada mais importa, porque a versão instalada em produção pode não ser a que você testou.
pnpm install --frozen-lockfile
No CI, sempre. pnpm install puro em pipeline é aceitar que uma versão nova entre sem revisão.
Desligar script de instalação. Boa parte dos ataques executa no postinstall, no momento da instalação, sem ninguém rodar nada.
# .npmrc
ignore-scripts=true
Isso quebra alguns pacotes que compilam binário. A saída é liberar por exceção, explicitamente. Chato, e é a medida mais eficaz da lista.
Janela de quarentena. Não instale versão publicada há menos de alguns dias. Boa parte dos comprometimentos é detectada e retirada em horas.
{ "minimumReleaseAge": 4320 }
Três dias de atraso custam nada e evitam a categoria inteira de “peguei a versão maliciosa nas primeiras horas”.
Atualização automática com revisão. Renovate ou Dependabot agrupando atualizações e abrindo PR. O ponto não é atualizar sozinho: é que a atualização vire rotina pequena em vez de evento grande e assustador.
Time que atualiza a cada dois anos fica em versão vulnerável por medo da migração, o que é um risco maior que o de atualizar.
Reduzir o número de dependências
A defesa mais subestimada. Cada pacote é um mantenedor a mais em quem você confia.
Antes de adicionar, três perguntas: quantas linhas eu escreveria pra fazer isso? Quantas dependências transitivas ele traz? Está sendo mantido?
Um pacote de vinte linhas que traz sete dependências transitivas é um mau negócio. Escrever as vinte linhas é menos código pra manter e sete relações de confiança a menos.
pnpm why <pacote> # quem trouxe isso pra cá
pnpm licenses list # o que entrou junto
SBOM e o que fazer com ele
Uma lista de tudo que compõe o seu artefato, em formato padrão.
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
Gerar é fácil. O valor está em ter isso disponível no dia em que sair uma vulnerabilidade crítica numa biblioteca e a pergunta for “a gente usa?”.
Sem SBOM, essa pergunta leva um dia e a resposta é incerta. Com SBOM versionado junto de cada release, leva um minuto e a resposta é exata, inclusive pra versões antigas ainda em produção.
Guarde o SBOM de cada versão publicada, não só da atual. É a diferença entre saber e achar.
O que auditar de verdade
npm audit gera ruído: aponta vulnerabilidade em dependência de desenvolvimento que nunca vai pra produção, e classifica como crítica coisa que não é explorável no seu contexto.
O filtro que eu uso: a vulnerabilidade está em código que roda em produção? Ela é alcançável a partir de entrada de usuário? Existe correção?
Se as três respostas forem sim, é prioridade. Se a primeira for não, entra na fila normal.
Tratar todo alerta como emergência treina o time a ignorar alertas, que é o pior desfecho possível.
O caso de quem publica
Se você mantém pacote, você é a superfície de ataque de outra pessoa.
Segundo fator obrigatório na conta do registro. Token de publicação com escopo mínimo e prazo curto. Publicação a partir do CI com proveniência, nunca da máquina de alguém. E, se houver mais de um mantenedor, revisão obrigatória antes de publicar.
O ponto que fica
Você não vai ler o código das suas dependências. Ninguém vai.
O que dá pra fazer é reduzir a quantidade delas, atrasar a adoção do que acabou de sair, impedir execução automática na instalação, e saber exatamente o que está lá dentro quando a notícia ruim chegar.
Isso é uma tarde de configuração e vale mais que qualquer ferramenta paga. Sobre o resto do básico, escrevi em como construir aplicações seguras.