Supply chain no npm está quebrado — o caso Axios explica por quê

🇺🇸 Read in English
Índice

Mais de 80 milhões de downloads por semana. Três horas de exposição. Um npm install era tudo o que separava o seu CI runner — com todas as suas credenciais de deploy — de um trojan de acesso remoto.

Em 30 de março de 2026, o Axios — provavelmente a biblioteca HTTP mais usada do ecossistema JavaScript — foi comprometido. Não por uma vulnerabilidade no código. Não por um bug no protocolo. Por um atacante que roubou credenciais de um mantenedor e publicou versões maliciosas direto no npm.

O mais perturbador: o modelo que permitiu esse ataque não é um bug. É como o npm foi desenhado para funcionar.

1) O que aconteceu

O ataque seguiu uma execução cirúrgica:

30/03 — O atacante publica plain-crypto-js@4.2.0 no npm. Versão limpa. Zero código malicioso. O objetivo: criar histórico no registry para o pacote não parecer suspeito.

31/03 (~18h depois) — Publica plain-crypto-js@4.2.1. Agora com um postinstall hook malicioso e um dropper obfuscado.

31/03 (mesmo dia) — Com credenciais roubadas do mantenedor principal do Axios (jasonsaayman), altera o email da conta para ifstap@proton.me e publica duas versões:

  • axios@1.14.1
  • axios@0.30.4

Ambas adicionam plain-crypto-js@4.2.1 como dependência direta no package.json. A dependência nunca é importada no código. É uma dependência fantasma — existe só para que o npm execute seu script de instalação.

~3 horas depois — Scanners (Socket.dev, npm) detectam e removem as versões maliciosas.

Três horas parece pouco. Não é.

O Axios tem mais de 80 milhões de downloads por semana. São milhares de downloads por minuto. Em ~3 horas de exposição, estamos falando de potencialmente dezenas de milhares de instalações — cada uma executando o malware automaticamente, sem nenhuma interação do desenvolvedor.

Em 1º de abril, o Google Threat Intelligence Group (GTIG) atribuiu o ataque ao UNC1069, um grupo com nexo na Coreia do Norte vinculado ao Lazarus Group (BlueNoroff). A Microsoft confirmou, referindo-se ao mesmo ator como Sapphire Sleet. Não foi um hacker oportunista. Foi uma operação de estado-nação com motivação financeira — o mesmo tipo de grupo por trás de roubos bilionários em exchanges de criptomoedas.

2) Anatomia do ataque

Fase 1: O pacote-isca

O atacante publicou uma versão limpa de plain-crypto-js um dia antes. Registries e scanners são menos agressivos com pacotes que já têm histórico. Uma versão limpa cria a ilusão de legitimidade.

Detalhe sutil: o nome plain-crypto-js imita o popular crypto-js. Não é typosquatting exato, mas se um humano vê essa dependência no diff, provavelmente não levanta a sobrancelha.

Fase 2: A dependência fantasma

Nas versões comprometidas do Axios, a única mudança foi no package.json:

{
  "dependencies": {
    "plain-crypto-js": "4.2.1"
  }
}

Nenhum import. Nenhum require. Nenhuma referência no código-fonte.

Isso é um sinal de alerta que deveria ser detectável programaticamente: uma dependência declarada no manifesto que nenhum arquivo do projeto usa.

Fase 3: O dropper

O plain-crypto-js@4.2.1 declarava um postinstall hook:

{
  "scripts": {
    "postinstall": "node setup.js"
  }
}

O setup.js era duplamente obfuscado — Base64 reverso + cifra XOR com chave OrDeR_7077. Quando decodificado, o script:

  1. Detectava o OS (Windows, Linux, macOS)
  2. Conectava ao C2: sfrclak[.]com:8000 (IP: 142.11.206.73)
  3. Baixava um payload específico para a plataforma
  4. Executava o payload
  5. Apagava as próprias evidências — substituía o setup.js e o package.json por versões limpas

Esse último passo é particularmente perigoso. Depois da execução, uma inspeção do node_modules não mostraria nada anormal. A infecção era invisível post-facto.

Fase 4: O RAT

O payload final era um Remote Access Trojan multiplataforma com:

  • Exfiltração de credenciais, tokens e secrets
  • Persistência (sobrevivia a reboots)
  • Canal de comunicação contínua com o C2
npm install axios@1.14.1
  └─> resolve plain-crypto-js@4.2.1
       └─> postinstall: node setup.js
            └─> detecta OS
            └─> GET sfrclak[.]com:8000/payload
            └─> executa RAT
            └─> apaga evidências

3) O alvo real não é sua máquina — é o seu CI

Vamos ser diretos sobre o impacto real.

Na máquina de um dev, esse ataque rouba credenciais locais. Ruim, mas contido.

No CI/CD, o estrago é outro nível.

Um CI runner típico tem acesso a:

  • AWS/GCP/Azure credentials via variáveis de ambiente — o RAT lê AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY direto do processo
  • Tokens de deploy para produção — incluindo acesso a clusters Kubernetes, registries de containers, buckets S3
  • Secrets de aplicação — chaves de API, tokens OAuth, certificados TLS
  • Tokens do GitHub/GitLab — que podem dar acesso de escrita a outros repositórios

Um atacante com acesso a esses secrets pode fazer deploy de código malicioso em produção. Pode exfiltrar dados de clientes. Pode pivotar para outros sistemas internos.

E aqui está o pior: muitos runners usam secrets persistentes em vez de credenciais temporárias. Um token de deploy que não expira e foi exfiltrado às 3h da manhã continua válido semanas depois — muito depois de alguém notar o ataque.

Isso não é roubo de credenciais de dev. É acesso potencial a toda a infraestrutura de produção de uma empresa.

4) O modelo de confiança que permite tudo isso

O problema não é o Axios. É o modelo.

Confiança transitiva exponencial

Quando você adiciona axios ao seu projeto, não está confiando só no Axios:

seu-projeto
  └─ axios (você confia nisso)
       └─ follow-redirects (você conhece isso?)
       └─ form-data
            └─ asynckit (e isso?)
            └─ combined-stream
                 └─ delayed-stream (e isso?)
            └─ mime-types
                 └─ mime-db (e isso?)
       └─ proxy-from-env
       └─ plain-crypto-js ← injetado pelo atacante

Cada dependência é um elo de confiança implícita. O atacante não precisou comprometer o Axios — só adicionou mais um elo na cadeia.

Um projeto JavaScript médio tem centenas de dependências transitivas. Você confia em todas elas. Você não auditou nenhuma.

Semver ranges: auto-update cego

A maioria dos projetos usa ^ no package.json:

{
  "axios": "^1.14.0"
}

Isso significa: “aceite qualquer versão patch ou minor a partir de 1.14.0”. Quando o atacante publicou 1.14.1, qualquer npm install subsequente instalou a versão maliciosa sem nenhuma mudança de código no seu projeto. Sem PR. Sem diff. Sem revisão.

Publicação fora do CI

O atacante publicou direto no npm com um token roubado. Isso bypassou completamente todo o CI/CD, code review e automação de segurança do repositório do Axios. O código nunca passou por um pull request.

O npm não exige que um pacote tenha sido construído em um pipeline verificável. Qualquer pessoa com um token válido publica o que quiser.

Postinstall hooks: execução automática de código arbitrário

O npm executa scripts de instalação automaticamente. É um recurso legítimo — pacotes com binários nativos precisam compilar. Mas é também o principal vetor de ataque em supply chain attacks.

Pense nisso: você roda npm install e o npm executa código arbitrário de centenas de pacotes no seu sistema. Com as permissões do seu usuário. Com acesso a todas as suas variáveis de ambiente.

Eu acho que o npm deveria desabilitar scripts por padrão. O custo operacional de habilitar scripts explicitamente para os poucos pacotes que realmente precisam é insignificante comparado ao risco de executar código arbitrário de centenas de dependências a cada install. O Bun já faz isso. O Deno nunca permitiu. O npm está atrasado.

Por que nada disso foi resolvido?

Esse modelo de confiança tem problemas conhecidos há quase uma década. Então por que ainda funciona assim?

Backwards compatibility. Desabilitar postinstall scripts por padrão quebraria milhares de pacotes legítimos. O npm não pode fazer isso sem um plano de migração massivo — e a organização nunca priorizou isso. Cada breaking change no npm afeta milhões de projetos. O resultado é inércia.

Governança descentralizada. O npm é mantido pelo GitHub (Microsoft), mas o ecossistema é descentralizado por design. Não existe uma autoridade central que possa impor regras de publicação mais rígidas sem resistência da comunidade. Propostas como “proveniência obrigatória” esbarram em mantenedores que publicam de suas máquinas pessoais — e que não querem (ou não podem) configurar pipelines de CI.

Incentivos econômicos. O npm Inc. (e agora o GitHub) lucra com adoção. Qualquer fricção no fluxo de publicação — 2FA obrigatório, proveniência obrigatória, cooldown de pacotes — reduz adoção na margem. O incentivo econômico é tornar a publicação o mais fácil possível. Segurança é custo, não receita.

O desenvolvedor médio não pensa nisso. A maioria dos engenheiros trata npm install como uma operação inerte — como baixar um arquivo. A percepção de risco é baixa porque os ataques são invisíveis quando funcionam. Só vira notícia quando alguém detecta. Quantos ataques menores passaram despercebidos?

O resultado é um ecossistema onde todos sabem que o modelo é frágil, mas ninguém tem incentivo suficiente para quebrá-lo e reconstruir. Até que o custo de não agir supere o custo de agir. O Axios talvez não seja esse ponto de inflexão. Mas cada incidente aproxima.

5) Isso não é novo — e está acelerando

IncidenteAnoVetorTécnica novaImpacto
event-stream2018Transferência de controle a contribuidor maliciosoSocial engineering de longo prazoRoubo de carteiras Bitcoin
ua-parser-js2021Credenciais npm roubadasAccount takeover diretoCryptominer + password stealer
colors.js / faker.js2022Sabotagem pelo mantenedor (protestware)Insider threat (sem invasão)Quebra de builds em massa
Shai-Hulud (chalk, debug)2025Phishing + worm auto-propagávelPropagação autônoma via CI/CD500+ pacotes, 2B downloads/semana
Axios2026Token clássico roubado + dependência fantasmaAnti-forensics + estado-naçãoRAT multiplataforma, 80M+ downloads/semana

O padrão é claro. A cada ano: pacotes maiores, técnicas mais sofisticadas, janelas menores.

A evolução é reveladora: ataques que antes dependiam de engenharia social (convencer um mantenedor a transferir controle) agora exploram automação, credenciais comprometidas e até propagação autônoma via CI/CD. O Shai-Hulud em 2025 era um worm. O Axios usou anti-forensics para apagar rastros. A próxima iteração vai ser pior.

O modelo atual de “publique rápido, escaneie depois” não é sustentável. O npm precisa evoluir para um modelo onde proveniência verificável é requisito, não opcional.

6) Como se proteger

Não existe bala de prata. A defesa é defesa em profundidade — camadas que, combinadas, reduzem o risco de forma significativa.

6.1) Estratégia de versionamento: lockfile + automação

Pinar todas as dependências em versão exata parece seguro, mas na prática cria outro problema: você para de receber patches de segurança legítimos. Dependências congeladas envelhecem rápido.

A abordagem que funciona em produção é lockfile rigoroso + atualização automatizada e controlada:

# CI/CD: sempre instalar do lockfile, nunca resolver versões novas
npm ci

No package.json, use semver ranges normalmente — mas nunca rode npm install no CI. O lockfile é sua âncora.

Para atualizações, use Renovate ou Dependabot para abrir PRs automáticos quando dependências têm novas versões. Isso dá:

  • Visibilidade: cada atualização vira um PR com diff, changelog e scan de segurança
  • Controle: você revisa antes de mergear — não é upgrade silencioso
  • Velocidade: patches de segurança legítimos chegam rápido

A regra: o lockfile é a verdade; atualizações são explícitas e auditáveis.

6.2) Desabilitar lifecycle scripts

Configure o .npmrc do projeto:

# .npmrc
ignore-scripts=true

Para os poucos pacotes que precisam de scripts (ex: esbuild, sharp, bcrypt), habilite explicitamente:

npm rebuild esbuild  # roda scripts só para este pacote

Isso bloqueia o vetor principal de supply chain attacks. O custo é mínimo — a vasta maioria dos pacotes não precisa de scripts.

6.3) Auditoria comportamental (não só CVEs)

# npm audit verifica CVEs conhecidas — necessário, mas insuficiente
npm audit

# Socket.dev analisa comportamento: scripts, rede, obfuscação
npx socket scan

A distinção é crítica. npm audit só encontra vulnerabilidades já catalogadas. O ataque ao Axios foi detectado pelo Socket.dev antes de ter um CVE — porque o scanner identificou comportamento anômalo: pacote novo com postinstall, código obfuscado, acesso de rede durante instalação.

Se você depende só de npm audit, está protegido apenas contra ataques do passado.

Sinais de alerta para revisão manual ou automatizada:

  • Dependência declarada mas não importada — dependência fantasma, exatamente como neste ataque
  • Postinstall script em pacote JavaScript puro — pacotes sem binários nativos raramente precisam de scripts
  • Código obfuscado — Base64, eval, atob, XOR em código de biblioteca é red flag
  • Acesso de rede durante instalação — fetch/http em scripts de install
  • Pacote criado recentemente como dependência de pacote popular — a combinação de “novo + usado por pacote grande” é suspeita
  • Mudança de email/ownership recente no mantenedor — sinal de account takeover

Nenhum desses sinais é prova de ataque isoladamente. Combinados, são quase certeza.

6.4) Proveniência verificável (SLSA e Sigstore)

Aqui preciso explicar melhor, porque isso é a defesa mais importante a longo prazo.

Proveniência (provenance) responde a uma pergunta simples: quem construiu esse pacote, a partir de qual código, em qual ambiente?

Sem proveniência, quando você instala axios@1.14.1, não tem como saber se foi construído pelo CI do repositório oficial ou se alguém rodou npm publish de um laptop comprometido. Os dois são indistinguíveis.

SLSA (Supply chain Levels for Software Artifacts) é um framework que define níveis de garantia. No nível mais útil para npm (SLSA Build L3), o pacote inclui uma atestação assinada que prova:

  • Foi construído em um runner de CI específico (ex: GitHub Actions)
  • A partir de um commit específico de um repositório específico
  • Sem intervenção humana no processo de build

Sigstore é a infraestrutura de assinatura que viabiliza isso. Usa certificados efêmeros ligados a identidades OIDC — não precisa gerenciar chaves GPG.

Na prática, isso permite verificar se o pacote foi gerado por um pipeline automatizado confiável — e não por alguém rodando npm publish de um laptop. No caso do Axios, as versões maliciosas foram publicadas manualmente, sem provenance válida. Um check de proveniência teria sido um forte indicador de comprometimento.

Vejamos como isso funciona:

# Verificar se os pacotes do seu projeto têm proveniência válida
npm audit signatures

Se o atacante do Axios tivesse publicado manualmente, como fez, a versão maliciosa não teria provenance statement. Um check automatizado no CI pegaria isso:

# No pipeline de CI
npm audit signatures || exit 1

Hoje, poucos pacotes publicam com proveniência. Mas isso está mudando — e deveria ser requisito do npm, não opcional. Um registry que aceita publicação anônima sem atestação de build é um registry que convida supply chain attacks.

6.5) Cooldown de pacotes

Não instale releases com menos de 72 horas automaticamente. O ataque ao Axios durou ~3 horas. Um cooldown de 24 horas teria sido suficiente para evitar a exposição.

Na prática, existem formas de implementar isso:

Com registry privado (Artifactory, Verdaccio, Nexus): configure uma política de quarentena que só espelha pacotes do npm público após N horas. O pacote existe no upstream, mas só fica disponível para os seus devs depois do período de cooldown. É a solução mais robusta para empresas.

Com Renovate: configure minimumReleaseAge no renovate.json para atrasar PRs de atualização:

{
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "minimumReleaseAge": "3 days"
    }
  ]
}

Isso não impede um dev de instalar manualmente, mas garante que atualizações automáticas só aconteçam depois do cooldown.

Com Dependabot: não há suporte nativo a cooldown, mas você pode combinar com um GitHub Action que verifica a data de publicação do pacote e bloqueia o merge se for recente demais.

O princípio: tempo é o melhor scanner. A maioria dos ataques de supply chain é detectada em horas ou dias. Um atraso intencional transforma você de vítima em observador.

6.6) Tokens granulares e publicação exclusiva via CI

O mantenedor do Axios tinha 2FA/MFA habilitado. Isso não ajudou. O vetor real foi um token clássico de longa duração (NPM_TOKEN) configurado como variável de ambiente no CI. Tokens clássicos do npm não exigem MFA para publicação — uma vez roubado, o atacante publica livremente.

Pior: o Axios já usava OIDC Trusted Publishing via GitHub Actions. Mas quando tanto o token clássico quanto credenciais OIDC estão presentes, o npm CLI prioriza o token. Isso tornou o OIDC ineficaz — o atacante publicou manualmente com o token roubado, contornando completamente as proteções do pipeline.

A lição: elimine tokens clássicos de longa duração. Use OIDC Trusted Publishing como único mecanismo de publicação. Revogue todos os tokens clássicos. Configure tokens granulares com escopo limitado (pacote específico, IP range, expiração curta) apenas quando OIDC não for possível. Verifique regularmente se tokens legados não estão esquecidos em variáveis de ambiente de CI.

7) O que eu faria como Tech Lead

Se eu fosse responsável pela segurança de supply chain de um time, implementaria isso amanhã:

1. Lockfile como contrato. npm ci no CI, sem exceção. PR que altera package-lock.json recebe label automática e revisão obrigatória.

2. Scripts desabilitados por padrão. .npmrc com ignore-scripts=true commitado no repositório. Allowlist explícita de pacotes que precisam de rebuild.

3. Scan comportamental no PR. Socket.dev ou equivalente rodando em cada PR que toca dependências. Não é substituto de npm audit — é complemento.

4. Proveniência como gate. npm audit signatures no pipeline. Pacote sem proveniência verificável em dependência crítica? Falha o build.

5. Secrets efêmeros no CI. Nada de AWS_SECRET_ACCESS_KEY persistente em variáveis de ambiente do runner. OIDC federation com AWS/GCP/Azure — credenciais que expiram em minutos, não meses.

6. Proxy/registry interno. Empresas maiores: considere um registry npm privado (Artifactory, Verdaccio) que aplica políticas antes do pacote chegar ao dev. Cooldown, scan obrigatório, allowlist.

O custo total disso? Uma ou duas sprints de configuração. O custo de não fazer? Pergunte para quem teve credenciais de produção exfiltradas por um npm install.

8) Para times de segurança e engenharia de plataforma

Se você é de um time de segurança ou plataforma, as recomendações acima são necessárias mas não suficientes. A questão para você não é “como meu projeto se protege” — é “como garanto que nenhum projeto da organização fique exposto”.

Registry interno com políticas. Um proxy npm (Artifactory, Nexus, Verdaccio) que fica entre os desenvolvedores e o npm público. O proxy aplica: cooldown obrigatório, scan comportamental antes de espelhar, allowlist de pacotes aprovados para dependências críticas. Nenhum pacote novo entra no ambiente da empresa sem passar por esse gate.

Espelhamento seletivo. Em vez de espelhar todo o npm, mantenha uma lista curada de pacotes aprovados. Pacotes fora da lista requerem aprovação explícita. Isso é mais restritivo, mas para organizações reguladas (fintech, saúde, infraestrutura crítica) pode ser o único modelo aceitável.

Observabilidade de dependências. Dashboards que mostram: quais pacotes foram atualizados nos últimos 7 dias em cada projeto? Quais têm proveniência verificável? Quais usam postinstall scripts? Isso transforma segurança de supply chain de reativa (“alguém foi comprometido?”) para proativa (“onde estamos expostos?”).

Política de secrets como código. Secrets no CI não devem ser configurados manualmente por devs. Use OIDC federation (AWS, GCP, Azure suportam nativamente) para que runners recebam credenciais efêmeras vinculadas ao workflow específico. Se um runner é comprometido, o token expirado em 15 minutos limita o blast radius drasticamente.

Incident response plan para supply chain. A pergunta não é “se” — é “quando”. Tenha um runbook pronto: como detectar que um pacote malicioso entrou no ambiente, como identificar quais projetos e environments foram afetados, como rotacionar secrets em massa, e como comunicar internamente. O Axios deu ~3 horas de janela. Sem automação, isso não é tempo suficiente para responder manualmente.

9) Se você foi afetado — checklist

Se o seu projeto rodou npm install entre 31/03/2026 e a remoção das versões maliciosas:

Verificar exposição

# Procurar no lockfile
grep -E "axios@(1\.14\.1|0\.30\.4)|plain-crypto-js" package-lock.json

# Verificar versão instalada
npm ls axios
npm ls plain-crypto-js

Remediar

# Downgrade para versão segura
npm install axios@1.14.0

# Limpar e reinstalar
rm -rf node_modules package-lock.json
npm install

Rotacionar TUDO

Toda credencial, token, API key e secret acessível na máquina ou ambiente afetado. Sem exceção:

  • AWS/GCP/Azure credentials
  • Tokens do GitHub/GitLab
  • Credenciais de banco de dados
  • SSH keys
  • Tokens de deploy
  • Secrets em variáveis de ambiente

Assuma comprometimento total. É mais barato rotacionar credenciais que não foram exfiltradas do que descobrir meses depois que sim, foram.

Auditar rede

# IOCs:
# Domínio: sfrclak[.]com
# IP: 142.11.206.73
# Porta: 8000

grep -E "sfrclak|142\.11\.206\.73" /var/log/syslog

Verifique também logs de firewall, proxy e DNS para conexões outbound ao C2.

10) Conclusão

O ecossistema npm foi construído para velocidade e conveniência. Instalar é trivial. Atualizar é automático. Publicar é rápido. Isso impulsiona produtividade. E é exatamente o que atacantes exploram.

Mas aqui está a pergunta que este incidente deveria forçar a comunidade a responder: de quem é a responsabilidade?

Do desenvolvedor individual que rodou npm install sem auditar 800 dependências transitivas? Do mantenedor que, mesmo com 2FA habilitado, tinha um token clássico de longa duração exposto no CI? Do npm que prioriza tokens legados sobre OIDC, executa scripts arbitrários por padrão e aceita publicação sem proveniência? Do modelo de financiamento que espera que voluntários mantenham infraestrutura crítica com segurança de nível enterprise?

A resposta confortável é “de todos”. A resposta honesta é que o modelo coloca a maior parte do ônus em quem tem menos poder para agir — o desenvolvedor final — enquanto quem controla o registry e define as regras padrão poderia resolver a maior parte do problema com mudanças de configuração.

As defesas existem. Lockfiles, proveniência, scan comportamental, scripts desabilitados, secrets efêmeros. Nenhuma é perfeita sozinha. Combinadas, transformam um ataque de três horas em uma não-ameaça.

Mas não deveria ser necessário montar uma fortaleza individual para compensar as fraquezas estruturais de uma plataforma.

O npm poderia, hoje, depreciar tokens clássicos de longa duração e exigir OIDC ou tokens granulares com expiração. Poderia desabilitar postinstall scripts por padrão e exigir opt-in explícito. Poderia tornar proveniência verificável um requisito para pacotes com mais de N downloads. Nenhuma dessas mudanças é tecnicamente impossível — o Bun e o Deno já implementaram variantes. O que falta é vontade política e a pressão de incidentes como este para forçar a mudança.

Um grupo de estado-nação comprometeu uma das bibliotecas mais usadas do mundo. A janela de três horas foi curta por sorte, não por design. A próxima pode não ser.

Enquanto isso, cada npm install continua sendo um ato de confiança. E confiança, em segurança, se verifica — não se assume.


Referências:

Comentários