Na última semana, o ecossistema de desenvolvimento JavaScript/Node.js foi atingido por um dos ataques de supply chain mais sofisticados já documentados. Um grupo de ameaças conhecido como TeamPCP comprometeu o popular scanner de vulnerabilidades Trivy e, menos de 24 horas depois, lançou um worm auto-propagável chamado CanisterWorm que infectou mais de 66 pacotes npm legítimos — impactando potencialmente milhares de pipelines CI/CD e máquinas de desenvolvedores ao redor do mundo.
Neste post, vou detalhar o que aconteceu, como o ataque funciona tecnicamente, e — mais importante — o que você precisa fazer agora para proteger seus sistemas em produção, ambientes de staging e máquinas de desenvolvimento.
O Que Aconteceu: A Timeline
Fase 1 — O Comprometimento do Trivy (Fevereiro–19 de Março)
O ataque começou no final de fevereiro de 2026, quando os atacantes exploraram uma misconfiguração nos workflows de GitHub Actions do Trivy. Um bot autônomo de IA chamado hackerbot-claw explorou um trigger pull_request_target mal configurado para roubar um Personal Access Token (PAT), estabelecendo acesso persistente à automação de releases do repositório, conforme documentado pela Upwind Security.
Em 1º de março, a equipe do Trivy divulgou o incidente e realizou rotação de credenciais. Porém, a rotação não foi completa: o atacante reteve acesso a credenciais que sobreviveram à troca, conforme confirmado pela própria Aqua Security.
Em 19 de março às 17:43 UTC, o TeamPCP usou a conta de serviço comprometida aqua-bot para:
- Force-push de 76 de 77 tags no repositório aquasecurity/trivy-action
- Modificação de todas as 7 tags no aquasecurity/setup-trivy
- Publicação do binário malicioso Trivy v0.69.4 via pipeline de release automatizado
O binário e as actions comprometidas foram distribuídos através de GitHub Releases, Docker Hub, GHCR (GitHub Container Registry), Amazon ECR Public, e repositórios deb/rpm. A janela de exposição durou aproximadamente 3 horas até a contenção às ~20:38 UTC.
O malware embutido coletava e exfiltrava silenciosamente:
- Tokens de API e credenciais cloud (AWS, GCP, Azure)
- Chaves SSH e tokens Kubernetes
- Configurações Docker e credenciais Git
- Variáveis de ambiente e secrets de CI/CD
A exfiltração ocorria via um domínio de typosquatting (scan.aquasecurtiy[.]org) e, como fallback, criava um repositório público chamado tpcp-docs na conta GitHub da vítima para staging dos dados roubados — uma técnica engenhosa, já que conexões ao github.com raramente são bloqueadas ou alertadas.
Fase 2 — O CanisterWorm Ataca o npm (20 de Março)
Menos de 24 horas depois, em 20 de março às 20:45 UTC, os tokens npm roubados foram transformados em arma. A Aikido Security detectou uma onda massiva de pacotes npm sendo comprometidos com um worm inédito: o CanisterWorm.
O ataque inicial comprometeu:
- 28 pacotes no scope @emilgroup
- 16 pacotes no scope @opengov
- Pacotes individuais: @teale.io/eslint-config, @airtm/uuid-base32, @pypestream/floating-ui-dom
A Socket posteriormente relatou que o ataque expandiu para 141 artefatos maliciosos em mais de 66 pacotes únicos, e a JFrog identificou versões comprometidas adicionais não reportadas anteriormente.
Anatomia Técnica do CanisterWorm
O CanisterWorm utiliza uma arquitetura de três estágios que merece atenção detalhada:
Estágio 1: Loader Node.js (postinstall hook)
Quando a vítima executa npm install de um pacote comprometido, um hook postinstall é acionado silenciosamente. Este script:
- Decodifica um payload base64 embutido (um script Python)
- Grava o script em ~/.local/share/pgmon/service.py
- Cria um serviço systemd de usuário em ~/.config/systemd/user/pgmon.service
- Ativa e inicia o serviço imediatamente
O serviço é configurado com Restart=always e RestartSec=5, garantindo persistência mesmo após reboots ou crashes. Todo o bloco é envolvido em try/catch — se a infecção falhar (ex: em Windows ou macOS), o npm install completa normalmente sem erro.
Todos os artefatos são disfarçados como ferramentas PostgreSQL: pgmon, pglog, .pg_state — nomes que passam despercebidos num terminal de desenvolvedor.
Estágio 2: Backdoor Python Persistente
O script Python é minimalista (usa apenas a stdlib) e:
- Dorme 5 minutos antes de qualquer ação — tempo suficiente para escapar de sandboxes automatizadas
- Consulta um ICP canister (smart contract no Internet Computer blockchain) a cada ~50 minutos usando User-Agent spoofado de navegador
- O canister retorna uma URL em texto plano apontando para o payload real
- Se a URL contiver youtube.com, o script ignora — este é o “kill switch” do modo dormante
- Caso contrário, baixa o binário para /tmp/pglog, marca como executável, e lança em processo detached
O uso de um ICP canister como dead-drop C2 é inédito em campanhas desse tipo. Como a infraestrutura é descentralizada na blockchain, não existe um ponto único de takedown — derrubar o canister exigiria uma proposta de governança e votação na rede, conforme explicou o pesquisador Charlie Eriksen da Aikido Security.
Além disso, o operador do canister pode trocar a URL a qualquer momento, enviando novos payloads para todas as máquinas infectadas sem tocar no implante local.
Estágio 3: O Worm Auto-Propagável
Na primeira onda, o componente de propagação (deploy.js) era um script separado que o atacante executava manualmente com tokens roubados. Porém, cerca de uma hora depois, uma mutação crítica apareceu nas versões 1.8.11 e 1.8.12 do @teale.io/eslint-config:
O novo index.js adicionou uma função findNpmTokens() que, durante o postinstall, varre automaticamente:
- Arquivos .npmrc do usuário (~/.npmrc), do projeto (./npmrc) e do sistema (/etc/npmrc)
- Variáveis de ambiente NPM_TOKEN, NPM_TOKENS, e qualquer variável contendo NPM e TOKEN
- Executa npm config get //registry.npmjs.org/:_authToken como subprocesso
Com os tokens coletados, o worm lança deploy.js como processo background detached. Este script:
- Autentica com cada token via /-/whoami
- Enumera todos os pacotes publicáveis da conta
- Incrementa o patch version automaticamente (1.54.0 → 1.54.1)
- Preserva o README original do pacote alvo (para manter aparências)
- Publica com –access public –tag latest, garantindo que a versão maliciosa seja o default
- 28 pacotes infectados em menos de 60 segundos
Este é o ponto onde o ataque salta de “conta comprometida publica malware” para “malware compromete mais contas e se publica sozinho”. Cada desenvolvedor ou pipeline CI que instala o pacote infectado e tem um token npm acessível torna-se vetor de propagação involuntário.
Segundo análise da Endor Labs, o worm confirma que auto-propagação tipo worm se tornou uma técnica recorrente em ataques de supply chain, não mais um incidente isolado.
Um detalhe curioso: o worm aparenta ter sido inteiramente “vibe-coded” usando ferramentas de IA — sem qualquer tentativa de ofuscação.
Indicadores de Comprometimento (IOCs)
Infraestrutura C2
| Indicador | Valor | Ação |
|---|---|---|
| ICP Canister C2 | tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io | Bloquear egresso para icp0.io no perímetro de rede |
| Domínio C2 primário | scan.aquasecurtiy[.]org (typosquat) | Bloquear no DNS/firewall |
| IP C2 | 45.148.10.212 | Bloquear no firewall |
| Tunnel C2 secundário | plug-tab-protective-relay.trycloudflare.com | Buscar em logs DNS |
| Repositório exfil | Repositório GitHub chamado tpcp-docs | Verificar criação não-autorizada na org |
Artefatos no Filesystem
| Caminho | Descrição |
|---|---|
| ~/.local/share/pgmon/service.py | Backdoor Python |
| ~/.config/systemd/user/pgmon.service | Unit de persistência systemd |
| /tmp/pglog | Payload binário baixado |
| /tmp/.pg_state | Arquivo de rastreamento de estado |
Hashes SHA256 do index.js Malicioso
| Hash | Descrição |
|---|---|
| e9b1e069efc778c1e77fb3f5fcc3bd3580bbc810604cbf4347897ddb4b8c163b | Wave 1: dry run (payload vazio, deploy manual) |
| 61ff00a81b19624adaad425b9129ba2f312f4ab76fb5ddc2c628a5037d31a4ba | Wave 2: backdoor ICP ativo, deploy manual |
| 0c0d206d5e68c0cf64d57ffa8bc5b1dad54f2dda52f24e96e02e237498cb9c3a | Wave 3: auto-propagante, payload de teste |
| c37c0ae9641d2e5329fcdee847a756bf1140fdb7f0b7c78a40fdc39055e7d926 | Wave 4: forma final (auto-propagante + backdoor ICP) |
Recomendações Práticas
Para Ambientes de Produção
1. Verificação imediata de exposição
# Verificar se o Trivy v0.69.4 está presente em qualquer lugar
find / -name “trivy” -exec {} –version \; 2>/dev/null | grep “0.69.4”
# Verificar artefatos do CanisterWorm
ls -la ~/.local/share/pgmon/ 2>/dev/null
ls -la ~/.config/systemd/user/pgmon.service 2>/dev/null
ls -la /tmp/pglog /tmp/.pg_state 2>/dev/null
# Verificar se o serviço pgmon está ativo
systemctl –user status pgmon.service 2>/dev/null
2. Remediação se comprometido
# Parar e desabilitar o serviço malicioso
systemctl –user stop pgmon.service
systemctl –user disable pgmon.service
# Remover artefatos
rm -f ~/.config/systemd/user/pgmon.service
rm -rf ~/.local/share/pgmon/
rm -f /tmp/pglog /tmp/.pg_state
# Recarregar systemd
systemctl –user daemon-reload
3. Rotacionar TODOS os secrets imediatamente
Se seu pipeline CI/CD executou o Trivy v0.69.4 ou instalou qualquer pacote comprometido, trate como comprometimento total de credenciais:
- Tokens npm (publish e read)
- Credenciais cloud (AWS access keys, GCP service accounts, Azure credentials)
- Chaves SSH e tokens Kubernetes
- Secrets do GitHub Actions
- Credenciais de container registries
- Qualquer secret acessível no ambiente de build
4. Bloquear IOCs no perímetro de rede
# Adicione às blocklists de DNS/firewall:
scan.aquasecurtiy.org
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io
*.icp0.io
plug-tab-protective-relay.trycloudflare.com
45.148.10.212
5. Auditar publicações npm
Se você é maintainer de pacotes npm, verifique se houve publicações não-autorizadas:
# Verificar versões publicadas de seus pacotes
npm view <seu-pacote> versions –json
# Verificar logs de acesso do npm
npm audit signatures
6. Pin de GitHub Actions por SHA
# INSEGURO – tag mutável pode ser redirecionada
– uses: aquasecurity/trivy-action@v0.35.0
# SEGURO – SHA imutável
– uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
Para Ambientes de Teste e Staging
1. Desabilitar scripts de lifecycle globalmente
# Desabilitar postinstall hooks que são o vetor de infecção
npm config set ignore-scripts true
# Quando precisar executar scripts de pacotes confiáveis:
npm rebuild <pacote-confiavel>
2. Usar ferramentas de allow-list para scripts
Considere usar @lavamoat/allow-scripts para controlar explicitamente quais pacotes podem executar hooks de lifecycle:
// package.json
{
“lavamoat”: {
“allowScripts”: {
“node-gyp-build”: true,
“esbuild”: true
}
}
}
3. Implementar cooldown de pacotes
Não instale pacotes recém-publicados imediatamente. Ferramentas como o npq auditam pacotes antes da instalação, e o pnpm suporta minimumReleaseAge para exigir uma idade mínima antes de aceitar uma versão.
4. Usar instalação determinística
# Sempre use npm ci (não npm install) em CI/CD
npm ci
# Com yarn
yarn install –frozen-lockfile
# Com pnpm
pnpm install –frozen-lockfile
5. Validar lockfiles
Use lockfile-lint para garantir que seus lockfiles não foram adulterados:
{
“scripts”: {
“lint:lockfile”: “lockfile-lint –path package-lock.json –type npm –allowed-hosts npm –validate-https”,
“preinstall”: “npm run lint:lockfile”
}
}
Para Ambientes de Desenvolvimento
1. Nunca armazene tokens npm em plain text
# Verifique se você tem tokens expostos
cat ~/.npmrc | grep “_authToken”
cat .npmrc | grep “_authToken”
env | grep -i “NPM.*TOKEN”
Se encontrar tokens plain text, rotacione-os imediatamente e migre para Trusted Publishing com OIDC (atualmente suportado no GitHub Actions e GitLab CI/CD) ou, no mínimo, granular tokens com validade curta (máximo 7 dias) e escopo restrito.
2. Habilitar 2FA obrigatório no npm
Acesse suas configurações em npmjs.com e:
- Habilite 2FA para todas as operações de escrita e publicação
- Use WebAuthn (hardware key) ao invés de TOTP quando possível
- Se não usa CI/CD para publicar, habilite 2FA sem permissão de bypass por token
3. Proteger diretórios systemd com políticas de sistema
# Com SELinux ou AppArmor, restrinja escrita em:
# ~/.config/systemd/user/
# Isso previne a instalação da persistência do CanisterWorm
4. Monitorar processos suspeitos
# Verificar se pgmon está rodando
ps aux | grep -E “pgmon|pglog|service.py”
# Verificar serviços systemd de usuário
systemctl –user list-units –type=service | grep -v “session”
# Verificar conexões de rede suspeitas
ss -tnp | grep -E “icp0.io|aquasecurtiy”
5. Considerar migração para pnpm
O pnpm é mais resistente a ataques de lockfile injection por design: não mantém URLs de tarball que possam ser modificadas maliciosamente, e recusa instalar pacotes no lockfile que não estejam declarados no package.json.
O Cenário Maior: Supply Chain como Vetor Recorrente
Este ataque não existe isoladamente. Ele faz parte de uma tendência acelerada de ataques à cadeia de suprimentos de software:
- Em fevereiro de 2026, a Socket documentou a campanha SANDWORM_MODE, que adicionou injeção de MCP servers maliciosos para atacar assistentes de código IA, além de um engine polimórfico usando DeepSeek Coder via Ollama para evadir detecção.
- Em março de 2025, o ataque ao tj-actions/changed-files comprometeu mais de 23.000 workflows GitHub, demonstrando como tags mutáveis são um vetor devastador.
- O próprio CanisterWorm inova ao usar blockchain ICP como infraestrutura C2 resistente a takedown, e ao ser o primeiro worm npm documentado com auto-propagação verdadeira.
A mensagem é clara: a segurança do supply chain não é opcional. Cada dependência que você adiciona, cada GitHub Action que referencia por tag, cada token que persiste numa variável de ambiente é uma superfície de ataque em potencial.
Checklist Rápido de Ações
Verificar se Trivy v0.69.4 ou pacotes comprometidos estão no seu ambiente
Verificar presença de artefatos pgmon/pglog no filesystem
Rotacionar todos os tokens npm, credenciais cloud e secrets de CI/CD
Bloquear IOCs (domínios, IPs, canister ICP) no firewall/DNS
Pinar GitHub Actions por SHA completo, nunca por tag
Desabilitar postinstall hooks ou usar allow-lists
Migrar de tokens npm long-lived para Trusted Publishing (OIDC)
Habilitar 2FA em todas as contas npm
Usar npm ci / –frozen-lockfile em todos os pipelines
Auditar publicações recentes de pacotes que você mantém
Referências e Leitura Adicional
| Recurso | Fonte | Link |
|---|---|---|
| Post-mortem oficial do incidente Trivy | Aqua Security | aquasec.com/blog/… |
| Análise técnica completa do CanisterWorm | Aikido Security | aikido.dev/blog/… |
| Análise de pacotes comprometidos | Socket | socket.dev/blog/… |
| Versões comprometidas adicionais detectadas | JFrog Security Research | research.jfrog.com/… |
| Análise do worm auto-propagável | Endor Labs | endorlabs.com/… |
| Breakdown do comprometimento de GitHub Actions | Socket | socket.dev/blog/… |
| Timeline completa do ataque ao Trivy | Upwind Security | upwind.io/feed/… |
| Cobertura do The Hacker News | The Hacker News | thehackernews.com/… |
| Cobertura do BleepingComputer | BleepingComputer | bleepingcomputer.com/… |
| Advisory GitHub oficial | Aqua Security | github.com/advisories/… |
| Discussão no repositório Trivy | Comunidade Trivy | github.com/aquasecurity/… |
| Recomendações npm Trusted Publishing | Datadog Security Labs | securitylabs.datadoghq.com/… |
| Roadmap de segurança do npm | GitHub Blog | github.blog/… |
| Best Practices de segurança npm | Liran Tal / GitHub | github.com/lirantal/… |
Este é um incidente em andamento. A Aqua Security confirmou em 23 de março de 2026 que nova atividade suspeita foi detectada no dia anterior, e a investigação forense está sendo conduzida com apoio da Sygnia. Tratar como campanha ativa, não como incidente contido.
