Shai-Hulud es un gusano autopropagable que infecta paquetes npm, un ataque a la cadena de suministro de software que recluta a cada víctima para seguir propagándolo. Se ejecuta cuando se instala un paquete, roba tokens de npm, tokens de GitHub y claves de la nube de la máquina, y usa el token de npm para publicar versiones infectadas de los demás paquetes de la víctima. Desde la primera oleada, en septiembre de 2025, ha vuelto repetidamente, y en mayo de 2026 sus autores publicaron el código fuente, de modo que cada oleada posterior puede ser de un grupo distinto.

La mayor parte del daño se produce en CI. En la oleada de noviembre de 2025, Wiz descubrió que solo el 23 % de las infecciones estaban en máquinas de desarrolladores: el otro 77 % estaba en pipelines de CI/CD, con GitHub Actions como el más común. Un build instala cientos de paquetes, contiene los secretos necesarios para publicar y desplegar, y ejecuta todos los scripts de instalación sin que nadie mire.

Cuando se anuncia una nueva oleada, todos los equipos se hacen la misma pregunta: ¿alguno de nuestros builds instaló una versión infectada? Este artículo explica qué hace el gusano, por qué la respuesta habitual («revise el lockfile») es incompleta y cómo responder correctamente.

Las oleadas hasta ahora

CuándoNombreQué tuvo de diferente
Septiembre de 2025Shai-HuludUn script postinstall ejecutaba un bundle.js de 3,6 MB, usaba TruffleHog para encontrar secretos y volvía a publicar hasta 20 paquetes del mantenedor. Se vieron afectados más de 500 paquetes, entre ellos @ctrl/tinycolor y 18 paquetes de @crowdstrike.
Noviembre de 2025Sha1-Hulud: The Second ComingPasó a preinstall, instalaba el runtime Bun para ejecutar una carga útil de 10 MB, introducía puertas traseras en hasta 100 paquetes por mantenedor y registraba las máquinas infectadas como runners autoalojados de GitHub. Se vieron afectados paquetes de Zapier, ENS, PostHog y Postman; Wiz contabilizó más de 25.000 repositorios con secretos filtrados.
Diciembre de 2025Prueba «3.0»Una versión modificada en un solo paquete, sin propagación significativa.
Mayo de 2026TanStack84 versiones maliciosas de 42 paquetes de @tanstack. El atacante nunca robó un token de npm: un workflow pull_request_target, una caché de Actions envenenada y un token OIDC leído de la memoria del runner le permitieron publicar mediante el trusted publishing de la propia TanStack.
Mayo de 2026AntV639 versiones de 323 paquetes. En los runners Linux de GitHub Actions, la carga útil leía los secretos de la memoria del proceso del runner, eludiendo el enmascaramiento de los logs. GitHub invalidó 61.274 tokens de escritura de npm.
Junio de 2026Miasma32 paquetes de @redhat-cloud-services, publicados mediante workflows inyectados con procedencia válida. Cada infección llevaba una carga útil cifrada de forma única, por lo que la detección basada en hashes falló.
Agosto de 2026CHAINDROPPaquetes de caché populares como keyv, cacheable y cache-manager, con cientos de paquetes afectados en total. Persistía mediante archivos de configuración de editores y asistentes de IA en el repositorio.

La carga útil volvió en septiembre de 2026, en cuatro paquetes que superaron el nuevo análisis de malware en el momento de la publicación de npm.

Qué hace el gusano en un job de CI

Los detalles cambian de una oleada a otra, pero el patrón se mantiene:

  1. Se ejecuta en la instalación. Un script preinstall o postinstall se ejecuta en cuanto npm install o npm ci llega a la versión infectada. Nada de su código tiene que importarlo.
  2. Recopila secretos. Variables de entorno, tokens de .npmrc, credenciales de la nube del servicio de metadatos y de los gestores de secretos, y todo lo que TruffleHog encuentre en disco. Las oleadas posteriores leían directamente la memoria del runner de GitHub Actions, lo que da acceso a secretos que nunca se expusieron al paso y que el enmascaramiento habría ocultado en los logs.
  3. Los saca fuera. Las primeras oleadas creaban repositorios públicos de GitHub con el nombre del gusano y subían un workflow que enviaba todos los secretos del repositorio a webhook.site. Las oleadas posteriores usaron servidores del atacante y redes de mensajería.
  4. Se propaga y se queda. Con un token de npm robado publica versiones infectadas de otros paquetes. Con un token de GitHub añade workflows, registra runners autoalojados o planta archivos de configuración que se ejecutan la próxima vez que alguien abre el repositorio.
  5. Se comporta de forma distinta en CI. La carga útil de noviembre de 2025 comprobaba GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1 y variables similares, y en CI se ejecutaba de forma síncrona, terminando su trabajo antes de que el build continuara.

Por qué el lockfile no lo aclara

El consejo habitual es buscar las versiones afectadas en package-lock.json, yarn.lock o pnpm-lock.yaml. Hágalo primero, pero un lockfile limpio no demuestra que el build lo esté:

  • Paquetes instalados fuera del lockfile. npx some-tool instala en la caché de npm un paquete que no es una dependencia del proyecto. npm install -g en un paso de configuración, una herramienta instalada dentro de un script de build y un npm install en un Dockerfile eluden el lockfile del proyecto.
  • El lockfile de la rama hoy no es el lockfile que se ejecutó. Un build de hace tres semanas usó lo que decía entonces el lockfile, en la rama que lo activó. Las pull requests de actualización de dependencias ejecutan CI con versiones nuevas antes de que nadie las revise.
  • Las cachés restauran lo que se instaló antes. Un node_modules o una caché de npm restaurados pueden contener una versión que el lockfile actual ya no menciona.
  • npm ci instala lo que dice el lockfile. Si la versión infectada ya estaba fijada, una instalación estricta la instala fielmente.
  • Una versión publicada correctamente puede seguir siendo maliciosa. TanStack y Miasma resultaron comprometidos a través de sus propios workflows de lanzamiento, y las versiones infectadas de Miasma llevaban procedencia válida. Comprobar que una versión se publicó correctamente no le dice que esté limpia.

Lo que realmente necesita es un registro de qué versiones de paquetes descargó cada build, en todos los repositorios y todas las ramas, durante todo el periodo en que una versión infectada estuvo en el registro.

Cómo comprobar si estuvo afectado

  1. Obtenga la lista de versiones afectadas de la oleada. Los proveedores de seguridad las publican en cuestión de horas; Datadog, por ejemplo, mantiene un repositorio de indicadores de compromiso con listas de paquetes y versiones.
  2. Busque en todos los lockfiles, en todas las ramas que ejecutaron CI durante la ventana, no solo en la rama por defecto.
  3. Busque en los propios builds. Busque en los logs de CI setup_bun.js, bun_environment.js u otros nombres de archivo citados en el aviso, y salida de instalación que mencione las versiones afectadas. Si conserva SBOM por build, busque en ellas las versiones.
  4. Revise su organización de GitHub en busca de repositorios nuevos con el nombre del gusano, archivos de workflow que usted no escribió (por ejemplo shai-hulud-workflow.yml, discussion.yaml o formatter_*.yml) y runners autoalojados que no registró.
  5. Borre las cachés. Elimine las cachés de GitHub Actions y la caché de npm en las máquinas de los desarrolladores y en los runners autoalojados, para que el siguiente build instale versiones limpias.
  6. Si algún build instaló una versión afectada, trate el job como comprometido. Rote todos los secretos que pudo leer: tokens de npm, tokens de GitHub, credenciales de la nube y claves de despliegue. Como las oleadas posteriores publicaron secretos robados en las cuentas de otras víctimas, no dé por hecho que está a salvo porque no haya aparecido nada en las suyas.

Cómo reducir el daño la próxima vez

  • Desactive los scripts de instalación donde pueda. npm v12, publicado en julio de 2026, desactiva los scripts de instalación por defecto y permite autorizarlos por paquete. pnpm y Yarn ofrecen controles similares.
  • Espere antes de adoptar versiones nuevas. La mayoría de las versiones infectadas se detectan y eliminan en cuestión de horas o días. Un periodo de espera en las actualizaciones de dependencias, que admiten tanto Dependabot como Renovate, las mantiene fuera de sus builds.
  • Publique con trusted publishing y tokens de corta duración. npm revocó todos los tokens clásicos en diciembre de 2025. Use trusted publishing basado en OIDC desde CI en lugar de tokens de larga duración, y mantenga los workflows de publicación separados de los de build y test.
  • Refuerce los workflows de los que abusa el gusano. Evite pull_request_target, fije las acciones a SHA de commits y conceda a GITHUB_TOKEN solo los permisos que necesite cada job. Consulte secretos de GitHub Actions: cómo se filtran y cómo protegerlos.
  • Limite adónde pueden conectarse los builds. Un build necesita sus registros de paquetes y unas pocas API. Si solo puede llegar a esos destinos, las llamadas del gusano a webhook.site o a sus propios servidores fallan. Consulte cómo detiene una política de egress la exfiltración desde GitHub Actions. Esto no detiene la exfiltración a través de un host que el build utiliza legítimamente, como la propia API de GitHub, de la que dependieron algunas oleadas.

Dónde encaja CRACI

CRACI ejecuta sus jobs de GitHub Actions en sus propios runners (runs-on: craci), y un proxy que entiende de paquetes en el runner registra todos los paquetes que cada job descarga realmente de npm y otros registros, incluidos los paquetes restaurados desde cachés. Ese registro cubre los puntos ciegos anteriores: las herramientas npx, las instalaciones globales y las instalaciones dentro de scripts de build aparecen todas, porque el registro procede de lo que descargó el job, no de lo que declaró un lockfile.

Cuando se anuncia una nueva oleada, usted busca las versiones afectadas en los builds que registró CRACI en lugar de reconstruirlas a partir de lockfiles y logs. Los hallazgos se agregan en todos los builds y repositorios, el inventario de CRACI muestra qué versiones entraron en qué productos y la traza de red de cada job muestra a qué se conectó, de modo que puede ver si un job que instaló una versión infectada llegó también a algún sitio al que no debía.

La política de egress de CRACI limita adónde puede conectarse un job, se valida antes de que empiece el job y, si falla, bloquea, y le avisa cuando un job intenta llegar a un destino que la política no permite. CRACI no puntúa los paquetes por comportamiento malicioso; combínelo con una herramienta de reputación de paquetes si quiere que las versiones infectadas se bloqueen antes de la instalación.

Reserve una demo para ver el registro de paquetes de uno de sus propios builds.