131% más CVE notificadas al día en 2026 que el año pasado.
← Todas las vulnerabilidades

Shai-Hulud: el gusano de npm y cómo comprobar si sus builds están afectados

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ándoOleadaQué 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. CISA publicó una alerta el 23 de septiembre.
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. Registrado como CVE-2026-45321 y añadido al catálogo KEV de CISA el 27 de mayo.
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.
Septiembre de 2026ReapariciónEl 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 2026tensorlakeEl 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:

  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.

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, postinstall y prepare, 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 script prepare ejecutaba la carga útil y después fallaba a propósito, de modo que npm la descartaba sin dejar rastro en node_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

  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ó.

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-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. 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

  1. 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.
  2. 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
  1. Fije una versión limpia y elimine los workflows, repositorios, runners y archivos de configuración que añadió el gusano.

Verificar la limpieza

  1. Todos los lockfiles resuelven a versiones limpias. En cada rama, npm ls <package> (o pnpm why o yarn why) muestra la versión instalada; ninguna puede estar en la lista de la oleada.
  2. 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
  1. 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.
  2. Las cachés han desaparecido. gh cache list no muestra ninguna caché creada antes de la limpieza en ningún repositorio afectado.
  3. 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.
  4. 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 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.

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:

OleadaIndicadoresFuente
Septiembre de 2025Carga ú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.siteStepSecurity, CISA
Noviembre de 2025setup_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 SHA1HULUDWiz
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.moeGHSA-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:

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.

Preguntas frecuentes

¿Sigue activo Shai-Hulud?

Sí. Ha vuelto por oleadas desde septiembre de 2025, y sus autores publicaron el código fuente en mayo de 2026, así que las oleadas posteriores pueden proceder de grupos distintos. La oleada más reciente que se trata aquí afectó al paquete tensorlake el 8 de octubre de 2026.

¿Tiene Shai-Hulud un CVE?

El gusano en su conjunto no tiene ninguno. GitHub publica avisos de malware para paquetes infectados concretos, y la oleada de TanStack de mayo de 2026 tiene el CVE-2026-45321, que CISA añadió a su catálogo Known Exploited Vulnerabilities el 27 de mayo de 2026.

¿Un lockfile limpio significa que no estuvimos afectados?

No. Los paquetes instalados con npx, las instalaciones globales, las instalaciones dentro de scripts de build y Dockerfiles, los lockfiles antiguos de otras ramas y las cachés restauradas pueden traer una versión infectada que el lockfile actual no muestra. Necesita un registro de lo que descargó realmente cada build.

¿Cómo compruebo si estoy afectado?

Obtenga las versiones de paquetes afectadas de la oleada, busque en todos los lockfiles de todas las ramas que ejecutaron CI en la ventana, busque las versiones y los nombres de archivo de la carga útil en los logs de CI y en las SBOM por build, y revise su organización de GitHub en busca de repositorios, workflows y runners autoalojados que usted no creó.

¿Qué hago si un build instaló una versión infectada?

Trate el job como comprometido. Rote todos los secretos que pudo leer, incluidos tokens de npm, tokens de GitHub, credenciales de la nube y claves de despliegue, elimine las cachés de GitHub Actions y de npm, y busque los workflows, repositorios y runners que añadió el gusano.

¿Cómo sé si el gusano se ejecutó en nuestros builds?

Busque en los logs de build, los lockfiles y los paquetes instalados los indicadores de la oleada: archivos de la carga útil como bundle.js, setup_bun.js, bun_environment.js o router_init.js, repositorios y workflows que usted no creó, un runner autoalojado llamado SHA1HULUD y conexiones a webhook.site o getsession.org. Cualquier job que instaló una versión afectada ejecutó la carga útil, así que trátelo como comprometido aunque no encuentre nada más.