Você não controla o que roda quando digita npm install. O comprometimento do TanStack no npm deixou isso dolorosamente claro. Se um dos mantenedores de JavaScript mais confiáveis pode ter seu canal de distribuição abusado, o seu pipeline também pode. Tratar o npm como um CDN é negligência organizacional.
Este post é um framework de decisão e um plano de rollout. Ele empresta urgência de duas manchetes adjacentes: o incidente de cadeia de suprimentos do TanStack e o relatório do Google de que grupos criminosos já estão usando IA para encontrar falhas graves de software mais rápido do que antes (link). Combinando essas realidades, a probabilidade de você executar sem saber um script malicioso de postinstall neste ano deixou de ser teórica.
A matemática desconfortável das cadeias de suprimentos de JavaScript
O registry do npm hospeda bem mais de dois milhões de pacotes. Um front‑end moderno ou serviço em Node típico referencia dezenas de dependências diretas e centenas — muitas vezes mais de mil — transitivas. Muitos trazem scripts de ciclo de vida que executam durante a instalação. Seu CI/CD, os laptops dos devs e os ambientes efêmeros de preview executam esse código automaticamente. Isso é execução remota de código por padrão.
Se a conta de um mantenedor popular é alvo de phishing, um token OAuth vaza ou um workflow de build é comprometido, versões maliciosas podem cair no seu grafo de dependências em minutos. E como os devs costumam permitir bumps de minor e patch, a deriva silenciosa é comum. É assim que typosquats, dependency confusion e sequestro de contas viram incidentes em nível organizacional.
Framework de decisão para CTO: três camadas de defesa
Você não elimina risco, mas muda os modos de falha. Seu objetivo é impedir que código desconhecido chegue à sua frota sem checagem e minimizar o raio de impacto quando isso acontecer.
Camada 1: Política e proveniência (decida o que pode entrar)
- Registry privado e curado como única saída. Coloque um proxy na frente do npm e faça dele sua fonte única da verdade. Opções: Verdaccio para times que querem um cache leve, ou registries enterprise como JFrog Artifactory, Sonatype Nexus ou AWS CodeArtifact. Bloqueie o acesso direto a registry.npmjs.org a partir do CI e das redes de dev.
- Allow‑list de pacotes e fixação de publicadores. Trate seu registry como um firewall de pacotes. Espelhe apenas os pacotes que você aprovar e, opcionalmente, fixe em identidades de publicadores conhecidas. Se um pacote muda de mantenedor ou é despublicado e republicado, exija revisão humana.
- Imponha lockfiles imutáveis e instalações determinísticas. Use npm ci (não npm install) no CI. Trave em versões exatas e faça commit do lockfile. Para pnpm ou Yarn, imponha modos de lockfile congelado. Só isso já impede atualizações surpresa de entrarem durante os builds.
- Exija proveniência de build para pacotes críticos. O GitHub suporta proveniência de pacotes no npm (baseada em Sigstore) que vincula o pacote publicado ao repositório e ao workflow de CI. Prefira pacotes que publicam com proveniência para tudo que estiver no seu caminho crítico.
- Namespace para pacotes internos para matar dependency confusion. Use um escopo privado @org e configure o npm para resolvê‑lo apenas do seu registry. Nunca publique nomes internos no npm público. Defina explicitamente o mapeamento do registry: npm config set @yourorg:registry https://your-registry.example.com/
Camada 2: Controles no pipeline (controle como entra)
- Desative scripts de ciclo de vida no CI por padrão. Instale com npm ci --ignore-scripts e rode uma fase mínima e revisada de scripts se necessário. A maioria dos pacotes não requer postinstall; os que requerem devem ser exceções com regras explícitas de permissão.
- Controle de egress de rede no CI. Permita apenas conexões de saída para seu registry privado e seus repositórios de artefatos. Bloqueie o acesso à internet para etapas de build que tocam dependências. Se seu runner na nuvem não consegue isso, migre para um runner que consiga ou auto‑hospede.
- Análise estática e comportamental na importação. Use múltiplos scanners: vulnerabilidades tradicionais (npm audit, Snyk), mantenibilidade e comportamento (Socket, Phylum) e higiene do projeto (OpenSSF Scorecards). Quebre o build para pacotes que de repente ganham scripts de instalação, código ofuscado ou operações inseguras de rede/arquivo.
- SBOMs são o básico. Gere um SBOM para todo build (CycloneDX ou SPDX). Armazene‑o junto do seu artefato. É assim que você responde “Estamos afetados?” em minutos, não dias, quando o próximo comprometimento surgir.
- Builds reprodutíveis. Containerize os builds com imagens base fixadas, versões do gerenciador de pacotes fixadas (use o Corepack para fixar npm/yarn/pnpm) e sem estado ambiente. Se você recompila o mesmo commit em um ambiente limpo e obtém um artefato diferente, você tem um problema de cadeia de suprimentos.
Camada 3: Controles de tempo de execução para reduzir o raio de impacto (assuma que vai passar)
- Modelo de permissões do Node. Versões recentes do Node incluem um modelo de permissões opt‑in que pode negar acesso a sistema de arquivos (fs) e rede (net) por padrão. Empacote seus processos com o conjunto mínimo de caminhos e hosts permitidos. Isso transforma um implante de supply chain bem‑sucedido em um no‑op barulhento e bloqueado.
- Sandboxing de contêiner e syscalls. Rode serviços em Node sob perfis seccomp/apparmor ou gVisor. Para jobs de build de front‑end, use contêineres não privilegiados sem o socket do docker. Um postinstall malicioso não deve conseguir dar docker pull, ssh ou curl para hosts arbitrários.
- Allow‑lists de egress em tempo de execução. Mesmo que o código rode, restrinja o tráfego de saída exatamente ao que o serviço precisa. Um beacon para um host de comando‑e‑controle vira uma falha de DNS bloqueada que você pode alertar.
- Rollback rápido e kill switches. Versione seu conjunto de dependências como você versione código. Se um incidente acontecer, você precisa de um único toggle de configuração para congelar atualizações de dependência, reverter para o último estado conhecido bom e invalidar caches comprometidos em minutos.
Um rollout concreto: 0–7, 30 e 90 dias
Semana 1: Estancar a sangria
- Congele a deriva. Imponha npm ci ou installs congelados de pnpm/yarn no CI imediatamente. Rejeite PRs que atualizam lockfiles sem revisão.
- Desligue scripts de instalação no CI. Adicione --ignore-scripts em todos os lugares. Crie uma allow‑list pequena para os raros pacotes que realmente precisam de scripts de instalação. Se seu build quebrar, é um sinal para substituir o pacote ou fazer vendor.
- Fixe o gerenciador de pacotes. Habilite o Corepack e fixe as versões de npm/pnpm/yarn no repositório. Sem isso, devs e runners diferentes se comportam de forma diferente.
- Gere SBOMs agora. Integre a geração de CycloneDX nos builds e armazene os artefatos com seus SBOMs. Isso leva um dia e paga dividendos toda vez que uma manchete estoura.
- Adicione regras de egress para o CI. Restrinja o tráfego de saída ao seu repositório de artefatos e ao seu registry. Bloqueie registry.npmjs.org diretamente se você já tem um proxy privado; se não, coloque o proxy no seu plano de duas semanas abaixo.
Dia 30: Erga o portão
- Coloque um registry privado de pé. Verdaccio é a rampa mais rápida; Artifactory/Nexus/CodeArtifact se você precisa de workflows enterprise. Espelhe apenas pacotes aprovados. Ative a quarentena de upstream para novos nomes até serem revisados.
- Adote varredura com múltiplos motores. Conecte Snyk ou OSV para CVEs, Socket ou Phylum para comportamento e OpenSSF Scorecards para higiene. Quebre o build quando o perfil de risco de um pacote saltar entre versões (ex.: novo script de instalação ou chamadas de rede adicionadas).
- Namespace todos os pacotes internos sob @yourorg. Atualize as configs do npm para direcionar o escopo apenas ao seu registry. Audite repositórios atrás de nomes internos sem escopo e corrija.
- Comece a registrar proveniência para seus próprios pacotes. Se você publica pacotes internos no seu registry privado, assine‑os ou anexe a proveniência do build. Se você publica open source, habilite a proveniência do npm via GitHub Actions e divulgue isso aos downstreams.
- Containerize e torne os builds reprodutíveis. Imagem base do Node fixada, gerenciador de pacotes fixado, lockfile congelado, zero rede exceto para o registry. Registre os digests das imagens nos metadados do build.
Dia 90: Reduza o raio de impacto
- Habilite o modelo de permissões do Node nos serviços. Comece com deny‑all e abra o conjunto mínimo de permissões de fs e net por serviço. Sim, você vai descobrir suposições permissivas demais; corrija.
- Endureça os runners de CI. Contêineres não privilegiados, sem socket do docker nas etapas de build, perfis seccomp e isolamento de sistema de arquivos para workspaces. Trate seus runners como ativos de produção, não brinquedos descartáveis.
- Allow‑lists de egress por serviço em tempo de execução. Defina domínios e portas conhecidos como bons na configuração. Faça cumprir via seu service mesh, firewall eBPF ou políticas de rede da nuvem.
- Formalize o rollback. Versione seus conjuntos de dependências, faça cache no registry e documente um procedimento de emergência: congelar espelhos, bloquear um pacote, reverter lockfiles e invalidar artefatos suspeitos.
- Exercite o drill. Rode um table‑top baseado em um incidente real recente. Cronometre as etapas: detecção, avaliação de impacto (obrigado, SBOM), quarentena, rollback e post‑mortem.
Decisões que você realmente precisa tomar
npm vs pnpm vs Yarn
Escolha um e padronize na empresa inteira com o Corepack. O store endereçado por conteúdo e a rigidez do pnpm são atraentes para monorepos, mas npm ci com lockfiles congelados é universalmente entendido. A escolha errada é permitir que três ferramentas coexistam.
Registry privado: construir vs comprar
- Verdaccio: implantação rápida, ótimo cache, controle simples de escopos. Perfeito para times seed‑to‑Series B. Você vai precisar adicionar scanners e políticas ao redor.
- Artifactory/Nexus/CodeArtifact: mecanismos de política mais profundos, melhor auditoria e autenticação enterprise. Mais para operar, mas a escolha certa se você tem dezenas de times e centenas de serviços.
O que bloquear por padrão
- Scripts de ciclo de vida no CI. Permite exceções via allow‑list.
- Código ofuscado em pacotes. Bloqueio rígido, a menos que revisado e internalizado (vendor).
- Novos mantenedores/publicadores em pacotes críticos. Em quarentena até revisão.
- Pacotes com compiladores nativos em postinstall (node-gyp) em caminhos críticos. Prefira prebuilts ou alternativas.
O que medir
- Taxa de determinismo das instalações. Porcentagem de builds em que as versões de dependências coincidem exatamente com o último estado conhecido bom. Meta: >99,5%.
- Tempo para resposta de impacto. Do momento da manchete até “quais serviços importam a versão X?”. Meta: minutos, usando busca por SBOM.
- Delta de deriva por semana. Quantos pacotes foram atualizados sem ticket explícito e revisão? Meta: próximo de zero.
- Eventos de egress bloqueados. Suas regras de egress no runtime e no CI estão de fato bloqueando hosts desconhecidos? Você quer barulho regular e explicável, não silêncio.
O que o incidente do TanStack realmente muda
Ele invalida a heurística “confiamos nos projetos top”. Mantenedores populares são precisamente os alvos de maior valor. Também prova que a velocidade de propagação supera a velocidade de comunicação. Quando um mantenedor avisa no Twitter ou publica um post‑mortem, versões maliciosas podem já estar nos seus caches e nos seus laptops.
E com relatos críveis de IA acelerando a descoberta e a manipulação de exploits, você deve esperar ataques mais frequentes e mais convincentes. Isso não significa pânico; significa que seu estado padrão é quarentena primeiro, explicação depois. Suas ferramentas decidem o que entra. Humanos explicam o porquê.
Como rodar isso com times nearshore
Se você lidera times distribuídos pelos US e Latin America, precisa dos mesmos controles em todo lugar. As práticas acima não atrasam times nearshore; aceleram, ao remover o drama das dependências. Veja como implementamos isso em times mistos US–Brazil:
- Um único registry, global. Todo mundo aponta para o mesmo espelho curado. 6–8 horas de sobreposição de fuso tornam o on‑call de operações do registry viável.
- Um único gerenciador de pacotes e versão fixada no repositório. Nada de “no meu npm funciona”.
- Stacks iniciais pré‑aprovados. Templates com dependências travadas, scanners, SBOM e regras de egress embutidas. Novos serviços já começam seguros por padrão.
- Memória muscular para incidentes. Rode o mesmo table‑top entre sites. Torne a busca por SBOM e os comandos de quarentena no registry algo de memória muscular.
Playbook: se outro comprometimento aparecer amanhã
- Identifique exposição em minutos. Pesquise o índice de SBOM pelo nome do pacote e versões. Sinalize serviços afetados.
- Quarentena no registry. Bloqueie as versões maliciosas, limpe caches e congele o espelhamento de upstream.
- Trave atualizações de dependência. Acione o kill switch para parar a movimentação de lockfiles nos repositórios por 24–48 horas.
- Rebuild a partir do último estado conhecido bom. Use o gerenciador fixado e lockfiles congelados para reproduzir o artefato de ontem. Compare os digests.
- Faça patch ou substitua. Se existir uma versão limpa com proveniência, promova‑a pelo registry e faça o bump de forma deliberada.
- Post‑mortem e atualização de política. O pacote ganhou scripts de instalação? Os mantenedores mudaram? Adicione uma regra para que a próxima tentativa seja bloqueada automaticamente.
Objeções comuns, respondidas
“Isso vai nos atrasar.”
Vai, por cerca de duas sprints. Depois, acelera. Os times que migramos para registries curados e lockfiles congelados passam dramaticamente menos tempo apagando incêndio de builds quebrados e regressões surpresa. Troque um setup único por estabilidade permanente.
“Não dá para bloquear scripts de instalação — precisamos de ferramentas afiadas.”
Ok. Faça uma allow‑list. Mas se 90% dos pacotes não precisam de scripts de ciclo de vida, por que deixar 100% deles rodarem shell arbitrário nos seus builders? Deixe a exceção provar que é excepcional.
“O open source anda rápido demais para allow‑lists.”
Então defina um caminho rápido com heurísticas automatizadas e revisão pós‑merge. Por exemplo: auto‑aprovar bumps de minor em que o Scorecards permaneça verde, o diff de comportamento esteja vazio e a proveniência intacta. Todo o resto espera por olhos humanos.
O custo de não fazer isso
Assuma que um único postinstall malicioso raspa seus segredos de CI e os envia para um pastebin. Seu tempo mediano para girar tokens em toda a sua plataforma é medido em dias. Sua distração total de engenharia vai apagar uma sprint ou mais. Se isso acontece uma vez por ano, o “tempo economizado” por evitar investimento em registry e políticas é ilusão.
O ponto não é tornar o desenvolvimento em JavaScript sem alegria. É parar de tratar o npm como um CDN onde velocidade supera escrutínio. Você jamais faria curl | bash em produção. npm install é curl | bash com UX melhor. Comece a agir de acordo.
Principais conclusões
- Coloque um registry privado e curado na frente do npm e faça dele a única saída para dependências.
- Imponha lockfiles imutáveis e instalações determinísticas com npm ci ou equivalente.
- Desabilite scripts de ciclo de vida no CI por padrão; permita via allow‑list apenas quando necessário.
- Adote varredura com múltiplos motores (CVEs, comportamento, higiene) e gere SBOMs para todo build.
- Habilite o modelo de permissões do Node e allow‑lists de egress em runtime para reduzir o raio de impacto.
- Padronize um único gerenciador de pacotes via Corepack e fixe versões em toda a organização.
- Pratique o drill de incidentes: busca por SBOM, quarentena no registry, rollback e comunicação.
- Espere ataques acelerados por IA; projete para quarentena primeiro, explicação depois.