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. Si alguno de sus builds instaló una versión infectada, trate ese job como comprometido y rote todos los secretos que pudo leer.
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 resto, sobre todo 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. Por eso, cuando se anuncia una nueva oleada, todos los equipos se hacen la misma pregunta: ¿alguno de nuestros builds instaló una versión infectada?
Qué ha pasado
| Cuándo | Oleada | Qué tuvo de diferente |
|---|---|---|
| Septiembre de 2025 | Shai-Hulud | Un 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. CISA publicó una alerta el 23 de septiembre. |
| Noviembre de 2025 | Sha1-Hulud: The Second Coming | Pasó 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 2025 | Prueba «3.0» | Una versión modificada en un solo paquete, sin propagación significativa. |
| Mayo de 2026 | TanStack | 84 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. Registrado como CVE-2026-45321 y añadido al catálogo KEV de CISA el 27 de mayo. |
| Mayo de 2026 | AntV | 639 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 2026 | Miasma | 32 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 2026 | CHAINDROP | Paquetes 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. |
| Septiembre de 2026 | Reaparición | El 7 de septiembre la carga útil volvió en cuatro paquetes que superaron el nuevo análisis de malware en el momento de la publicación de npm. |
| Octubre de 2026 | tensorlake | El 8 de octubre se publicó [email protected] con el gusano. OX Security observó nuevas claves del atacante y apunta a «un actor de amenazas independiente o un grupo nuevo que usa el nombre Shai-Hulud». |
Qué hace el gusano en un job de CI
Los detalles cambian de una oleada a otra, pero el patrón se mantiene:
- Se ejecuta en la instalación. Un script
preinstallopostinstallse ejecuta en cuantonpm installonpm cillega a la versión infectada. Nada de su código tiene que importarlo. - 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. - 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. - 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.
- Se comporta de forma distinta en CI. La carga útil de noviembre de 2025 comprobaba
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1y variables similares, y en CI se ejecutaba de forma síncrona, terminando su trabajo antes de que el build continuara.
Quién está afectado
Cualquiera cuyas máquinas o builds instalaran una versión infectada mientras estaba en el registro: portátiles de desarrolladores, jobs de CI, builds de Docker que ejecutan npm install y runners autoalojados. No hace falta depender directamente del paquete; basta con una dependencia transitiva o una herramienta ejecutada con npx. Cada oleada tiene su propia lista de paquetes y versiones, y las versiones infectadas suelen retirarse en cuestión de horas o días, así que lo que importa es si algo las instaló durante esa ventana.
Cómo funciona
Shai-Hulud no es un fallo de npm. Es código malicioso publicado dentro de paquetes por lo demás legítimos, que los avisos de malware de GitHub para los paquetes infectados clasifican como código malicioso incrustado (CWE-506). Cada oleada combina varias técnicas:
- Ejecución durante la instalación. npm ejecuta los scripts de ciclo de vida de un paquete, como
preinstall,postinstallyprepare, durante la instalación, con los permisos del usuario o del job de CI. Basta con que una versión infectada esté en el árbol de dependencias; nada tiene que importarla. - Propagación con derechos de publicación robados. Con un token de npm o de GitHub sustraído a una víctima, el gusano publica nuevas versiones de los demás paquetes de esa víctima con la carga útil añadida, de modo que cada mantenedor infectado infecta más paquetes.
- Publicación a través del propio pipeline de lanzamiento de la víctima (TanStack, mayo de 2026). Según el aviso de GitHub, el atacante encadenó un workflow
pull_request_target, una caché de GitHub Actions envenenada a través de la frontera de confianza entre el fork y el repositorio base, y un token OIDC leído de la memoria del runner, y publicó mediante el trusted publishing de TanStack sin modificar el workflow de publicación. Las versiones infectadas declaraban una dependencia opcional de un commit en un fork, cuyo scriptprepareejecutaba la carga útil y después fallaba a propósito, de modo que npm la descartaba sin dejar rastro ennode_modules. - Procedencia válida (Miasma, junio de 2026). Workflows inyectados publicaron desde los propios repositorios de las víctimas, así que las versiones infectadas llevaban procedencia válida, y una carga útil cifrada de forma única en cada infección burlaba la detección basada en hashes.
Cómo comprobar si está afectado
- 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.
- Busque en todos los lockfiles, en todas las ramas que ejecutaron CI durante la ventana, no solo en la rama por defecto.
- Busque en los propios builds. Busque en los logs de CI
setup_bun.js,bun_environment.jsu 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. - 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.yamloformatter_*.yml) y runners autoalojados que no registró.
Comandos que puede adaptar. Para buscar en los lockfiles de todas las ramas remotas, ponga las versiones afectadas de la oleada en affected.txt, una name@version por línea:
git fetch --all --quiet
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
while read -r pkg; do
name=${pkg%@*}; version=${pkg##*@}
git grep -n -A3 -F "$name" "$ref" -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' \
| grep -F "$version" | sed "s|^|$pkg |"
done < affected.txt
done
Para listar los runners autoalojados de su organización, los repositorios nuevos que mencionan el gusano y los archivos de workflow que se sabe que crea el gusano (sustituya ORG):
gh api --paginate orgs/ORG/actions/runners --jq '.runners[] | [.name, .os, .status] | @tsv'
gh repo list ORG --limit 1000 --json name,description,createdAt \
--jq '.[] | select((.description // "") | test("hulud"; "i")) | [.name, .createdAt] | @tsv'
for repo in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "repos/ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null \
| grep -E '^(shai-hulud-workflow\.yml|discussion\.yaml|formatter_.*\.yml)$' \
| sed "s|^|$repo |"
done
Los repositorios también pueden tener sus propios runners (gh api repos/ORG/REPO/actions/runners), y las primeras oleadas crearon repositorios en las cuentas personales de los desarrolladores cuyos tokens robaron, así que pida a sus desarrolladores que revisen las suyas.
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-toolinstala en la caché de npm un paquete que no es una dependencia del proyecto.npm install -gen un paso de configuración, una herramienta instalada dentro de un script de build y unnpm installen 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_moduleso una caché de npm restaurados pueden contener una versión que el lockfile actual ya no menciona. npm ciinstala 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. Consulte ¿estamos afectados? cómo evaluar un nuevo CVE en todos sus builds.
Cómo corregirlo y qué hacer si aún no puede aplicar el parche
- 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.
- 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:
gh cache delete --all --repo ORG/REPO # GitHub Actions caches, per repository
npm cache clean --force
rm -rf node_modules "$(pnpm store path)" # pnpm; for Yarn: yarn cache clean
- Fije una versión limpia y elimine los workflows, repositorios, runners y archivos de configuración que añadió el gusano.
Verificar la limpieza
- Todos los lockfiles resuelven a versiones limpias. En cada rama,
npm ls <package>(opnpm whyoyarn why) muestra la versión instalada; ninguna puede estar en la lista de la oleada. - Una instalación limpia no contiene archivos de la carga útil. Tras la purga de cachés anterior, vuelva a instalar y busque los archivos de la carga útil citados en los avisos:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- Las credenciales antiguas ya no funcionan. Los tokens de npm revocados ya no aparecen en
npm token list, los tokens de GitHub y las claves de despliegue antiguos se han eliminado, y las claves de la nube están desactivadas, no solo sustituidas. - Las cachés han desaparecido.
gh cache listno muestra ninguna caché creada antes de la limpieza en ningún repositorio afectado. - No queda nada de lo que añadió el gusano: ni repositorios, ni archivos de workflow, ni runners autoalojados, ni archivos de configuración de editores y asistentes de IA que usted no creara.
- El siguiente build está limpio. Compruebe las versiones que instaló realmente, a partir de su log o de su SBOM por build, no solo del lockfile.
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. El package firewall de GitHub, en versión preliminar pública para GitHub Enterprise Cloud desde el 20 de octubre de 2026, revisa las instalaciones de npm en busca de paquetes maliciosos y también ofrece periodos de espera.
- 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 aGITHUB_TOKENsolo 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.siteo 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.
Comprobar si ha habido un compromiso
Shai-Hulud es malicioso por diseño: cualquier máquina o job que instaló una versión infectada lo ejecutó. Los indicadores varían según la oleada, así que use el aviso de la oleada que esté comprobando. De fuentes primarias:
| Oleada | Indicadores | Fuente |
|---|---|---|
| Septiembre de 2025 | Carga útil bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflow .github/workflows/shai-hulud-workflow.yml en una rama shai-hulud; un repositorio público llamado Shai-Hulud; conexiones a webhook.site | StepSecurity, CISA |
| Noviembre de 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 o 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repositorios con cloud.json, contents.json, environment.json, truffleSecrets.json y actionsSecrets.json; workflows .github/workflows/discussion.yaml y formatter_*.yml; un runner autoalojado llamado SHA1HULUD | Wiz |
| Mayo de 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" en optionalDependencies; carga útil router_init.js (unos 2,3 MB) en la raíz del paquete; auxiliar tanstack_runner.js; conexiones a filev2.getsession.org y de seed1.getsession.org a seed3.getsession.org, y descargas de segunda fase desde litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Las oleadas posteriores cambiaban la carga útil en cada infección, así que un hash de archivo que no coincide no demuestra nada; compare primero las versiones de los paquetes.
Cuándo dar por hecho el compromiso. Trate como comprometidos todos los hosts y jobs de CI que instalaron una versión afectada, aunque la instalación fallara o no dejara nada en node_modules. El aviso de GitHub sobre TanStack dice que cualquier entorno de desarrollo o de CI que instalara una versión afectada «debe considerarse comprometido». Compruebe desde la primera publicación de la versión infectada, no desde el día en que se anunció la oleada.
Rote las credenciales. Rote todo lo que la instalación pudiera alcanzar. En la oleada de TanStack eso significó credenciales de AWS, GCP, Kubernetes, Vault, GitHub, npm y SSH, según el análisis posincidente. Revise los logs de auditoría de la nube en busca de actividad de los hosts afectados durante y después de la ventana de instalación.
Purgue cachés y artefactos. Busque en los repositorios de artefactos y en los proxies de dependencias copias en caché de las versiones afectadas y elimínelas, como aconseja CISA. Elimine node_modules, la caché de npm y las cachés de CI; en la oleada de TanStack, la vía de entrada del atacante fue una caché envenenada del store de pnpm, así que revise las cachés de Actions además de las cachés de paquetes. Reconstruya y vuelva a publicar todo lo que haya producido un job comprometido.
Qué decir a sus clientes
Si entrega software construido con npm, sus clientes le preguntarán si su producto lleva el gusano o si su pipeline de lanzamiento resultó comprometido. Responda con lo que comprobó y lo que hizo:
Hemos comprobado todos los builds de [producto] entre el [fecha] y el [fecha] con las listas publicadas de versiones de paquetes npm infectadas por Shai-Hulud. [Ningún build instaló una versión afectada.] / [Los builds de [componente] del [fecha] instalaron [paquete@versión]; hemos rotado las credenciales a las que podían acceder esos builds, hemos reconstruido [producto] con dependencias limpias y hemos publicado [versión]. Las versiones [x a y] están afectadas; actualice a [versión].] Actualizaremos este aviso si nuevas oleadas cambian la situación.
Si todavía lo está comprobando, dígalo, e indique cuándo responderá. Para una primera declaración y plantillas para cada resultado, consulte cómo responder a los clientes tras un ataque a la cadena de suministro.
Cómo ayuda CRACI
CRACI ejecuta sus jobs de GitHub Actions en sus propios runners (runs-on: craci) y 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, y la traza de red de cada job muestra a qué se conectó. El historial de builds conserva todos los builds anteriores con su SBOM, de modo que ve qué builds contenían una versión infectada y la ventana en la que estuvo expuesto, dentro de su periodo de retención.
La política de egress de CRACI limita adónde puede conectarse un job, se valida antes de que empiece el job y, si no puede aplicarse, bloquea la conexión (fail closed), y le avisa cuando un job intenta llegar a un destino que la política no permite. Las versiones infectadas con un CVE publicado, como las de la oleada de TanStack, se señalan como cualquier otra vulnerabilidad, y las policy gates pueden bloquear los builds que las contengan. CRACI no puntúa los paquetes por comportamiento malicioso ni señala una nueva versión infectada antes de que exista un aviso; combínelo con una herramienta de reputación de paquetes si quiere que las versiones infectadas se bloqueen antes de la instalación.
Vea el registro de paquetes de uno de sus propios builds
Otros incidentes: todas las vulnerabilidades.
Fuentes y actualizaciones
Fuentes primarias e investigación original:
- CISA: alerta del 23 de septiembre de 2025, catálogo KEV
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), nuestro plan para una cadena de suministro de npm más segura, revocación de los tokens clásicos de npm
- Análisis posincidente de TanStack
- Wiz sobre la oleada de noviembre de 2025 y sus consecuencias
- Microsoft sobre AntV y la guía de detección de Microsoft
- Datadog y Elastic sobre CHAINDROP
- StepSecurity sobre la oleada de septiembre de 2025
- FIRST: puntuación EPSS de CVE-2026-45321
Actualizaciones:
- 2026-11-11: Se han añadido al recuadro el EPSS, la clase CWE y el crédito, comprobaciones listas para copiar y pegar, y secciones sobre cómo funciona el gusano, cómo verificar la limpieza y cómo comprobar si ha habido un compromiso, con indicadores por oleada.
- 2026-11-11: Trasladado del blog a la sección de vulnerabilidades. Se añaden la oleada de tensorlake del 8 de octubre de 2026, la fecha de la reaparición de septiembre de 2026, el CVE-2026-45321 y su inclusión en el KEV para la oleada de TanStack, y el package firewall de GitHub.
- 2026-09-25: Primera publicación en el blog de CRACI.