CanisterWorm: O Worm Auto-Propagável que Infectou Mais de 60 Pacotes npm Após Comprometimento do Trivy

Tecnologia

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:

  1. Decodifica um payload base64 embutido (um script Python)
  2. Grava o script em ~/.local/share/pgmon/service.py
  3. Cria um serviço systemd de usuário em ~/.config/systemd/user/pgmon.service
  4. 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:

  1. Dorme 5 minutos antes de qualquer ação — tempo suficiente para escapar de sandboxes automatizadas
  2. Consulta um ICP canister (smart contract no Internet Computer blockchain) a cada ~50 minutos usando User-Agent spoofado de navegador
  3. O canister retorna uma URL em texto plano apontando para o payload real
  4. Se a URL contiver youtube.com, o script ignora — este é o “kill switch” do modo dormante
  5. 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:

  1. Autentica com cada token via /-/whoami
  2. Enumera todos os pacotes publicáveis da conta
  3. Incrementa o patch version automaticamente (1.54.0 → 1.54.1)
  4. Preserva o README original do pacote alvo (para manter aparências)
  5. Publica com –access public –tag latest, garantindo que a versão maliciosa seja o default
  6. 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

IndicadorValorAção
ICP Canister C2tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.ioBloquear egresso para icp0.io no perímetro de rede
Domínio C2 primárioscan.aquasecurtiy[.]org (typosquat)Bloquear no DNS/firewall
IP C245.148.10.212Bloquear no firewall
Tunnel C2 secundárioplug-tab-protective-relay.trycloudflare.comBuscar em logs DNS
Repositório exfilRepositório GitHub chamado tpcp-docsVerificar criação não-autorizada na org

Artefatos no Filesystem

CaminhoDescrição
~/.local/share/pgmon/service.pyBackdoor Python
~/.config/systemd/user/pgmon.serviceUnit de persistência systemd
/tmp/pglogPayload binário baixado
/tmp/.pg_stateArquivo de rastreamento de estado

Hashes SHA256 do index.js Malicioso

HashDescrição
e9b1e069efc778c1e77fb3f5fcc3bd3580bbc810604cbf4347897ddb4b8c163bWave 1: dry run (payload vazio, deploy manual)
61ff00a81b19624adaad425b9129ba2f312f4ab76fb5ddc2c628a5037d31a4baWave 2: backdoor ICP ativo, deploy manual
0c0d206d5e68c0cf64d57ffa8bc5b1dad54f2dda52f24e96e02e237498cb9c3aWave 3: auto-propagante, payload de teste
c37c0ae9641d2e5329fcdee847a756bf1140fdb7f0b7c78a40fdc39055e7d926Wave 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

  • uncheckedVerificar se Trivy v0.69.4 ou pacotes comprometidos estão no seu ambiente
  • uncheckedVerificar presença de artefatos pgmon/pglog no filesystem
  • uncheckedRotacionar todos os tokens npm, credenciais cloud e secrets de CI/CD
  • uncheckedBloquear IOCs (domínios, IPs, canister ICP) no firewall/DNS
  • uncheckedPinar GitHub Actions por SHA completo, nunca por tag
  • uncheckedDesabilitar postinstall hooks ou usar allow-lists
  • uncheckedMigrar de tokens npm long-lived para Trusted Publishing (OIDC)
  • uncheckedHabilitar 2FA em todas as contas npm
  • uncheckedUsar npm ci / –frozen-lockfile em todos os pipelines
  • uncheckedAuditar publicações recentes de pacotes que você mantém

Referências e Leitura Adicional

RecursoFonteLink
Post-mortem oficial do incidente TrivyAqua Securityaquasec.com/blog/…
Análise técnica completa do CanisterWormAikido Securityaikido.dev/blog/…
Análise de pacotes comprometidosSocketsocket.dev/blog/…
Versões comprometidas adicionais detectadasJFrog Security Researchresearch.jfrog.com/…
Análise do worm auto-propagávelEndor Labsendorlabs.com/…
Breakdown do comprometimento de GitHub ActionsSocketsocket.dev/blog/…
Timeline completa do ataque ao TrivyUpwind Securityupwind.io/feed/…
Cobertura do The Hacker NewsThe Hacker Newsthehackernews.com/…
Cobertura do BleepingComputerBleepingComputerbleepingcomputer.com/…
Advisory GitHub oficialAqua Securitygithub.com/advisories/…
Discussão no repositório TrivyComunidade Trivygithub.com/aquasecurity/…
Recomendações npm Trusted PublishingDatadog Security Labssecuritylabs.datadoghq.com/…
Roadmap de segurança do npmGitHub Bloggithub.blog/…
Best Practices de segurança npmLiran Tal / GitHubgithub.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.