El 11 de mayo de 2026, entre las 19:20 y las 19:26 UTC, ocurrió el ataque a la cadena de suministro más sofisticado que ha visto el ecosistema npm. En solo seis minutos, se publicaron 84 versiones maliciosas en 42 paquetes del namespace @tanstack. No fue un hacker que robó credenciales. Fue el propio pipeline legítimo de TanStack, usando su identidad verificada, ejecutando código que nadie había escrito. Y desde ahí, el gusano se propagó a Mistral AI, UiPath, OpenSearch y más de 160 paquetes adicionales.
¿La ironía más cruel? Los paquetes maliciosos llevaban firmas SLSA de provenance válidas. La herramienta diseñada para decirnos "este paquete es seguro" dijo exactamente lo contrario.
Cómo pasó: tres vulnerabilidades encadenadas
El ataque —atribuido al grupo TeamPCP y bautizado como "Mini Shai-Hulud"— no explotó un bug. Explotó tres comportamientos perfectamente documentados que, combinados, fueron letales.
Paso 1: El PR que nadie sospecharía
El 10 de mayo, un atacante creó un fork del repositorio TanStack/router bajo una cuenta nueva. Abrió un pull request con un título inofensivo: "WIP: simplify history build". El código modificado parecía trivial, pero el workflow de GitHub Actions del repositorio usaba el trigger pull_request_target.
Aquí está el problema: pull_request_target ejecuta el workflow con los permisos del repositorio base, no del fork. O sea, el código del atacante —que venía de un fork externo— corría con acceso a los secretos, el caché y los tokens del repositorio oficial de TanStack. Los maintainers habían intentado limitar los permisos, pero subestimaron un detalle crítico: actions/cache guarda datos usando un token interno del runner, no el GITHUB_TOKEN del workflow. Los permisos de solo lectura no protegen el caché.
Paso 2: Envenenar el caché de GitHub Actions
El código malicioso en el PR no exfiltró datos. Hizo algo más sutil: envenenó el caché de pnpm bajo una clave predecible —calculada desde el pnpm-lock.yaml público— que el workflow de release usaría horas después. El caché envenenado pesaba 1.1 GB y contenía binarios modificados. GitHub lo guardó sin cuestionar.
Ese caché permaneció dormido durante casi ocho horas, hasta que un push legítimo a main disparó el workflow de release. El workflow restauró el caché envenenado, y los binarios del atacante empezaron a ejecutarse dentro del runner oficial de TanStack.
Paso 3: Robar el token OIDC desde la memoria del proceso
El workflow de release tenía el permiso id-token: write, necesario para publicar en npm usando OIDC trusted publishers. Los binarios maliciosos usaron una técnica documentada desde marzo 2025 en el ataque tj-actions/changed-files: leer /proc/<pid>/mem del proceso runner para extraer el token OIDC de la memoria. Con ese token, publicaron directamente a npm autenticados como TanStack.
Los tests del workflow fallaron. El paso de publicación legítima nunca se ejecutó. Pero los paquetes maliciosos ya estaban en npm, firmados con SLSA provenance nivel 3. Para cualquier herramienta de seguridad, esos paquetes eran perfectamente legítimos.
El gusano que se propaga solo
El payload de cada paquete malicioso —un archivo de 2.3 MB llamado router_init.js— hacía cuatro cosas al instalarse:
Robaba credenciales: tokens de GitHub, tokens de npm, credenciales de AWS (vía IMDSv2), GCP, Azure, service accounts de Kubernetes, tokens de HashiCorp Vault, y todas las variables de entorno del sistema.
Se auto-propagaba: identificaba otros paquetes npm donde la víctima tuviera acceso de publicación, les inyectaba la misma dependencia maliciosa y publicaba nuevas versiones comprometidas. Cada desarrollador o CI runner infectado se convertía en un nuevo vector de infección.
Instalaba un wiper persistente: si encontraba un token de GitHub válido con acceso de escritura, instalaba un daemon llamado
gh-token-monitorque consultaba GitHub cada 60 segundos. Si el token era revocado, el daemon ejecutabarm -rf ~/—borrando todo el directorio home del usuario. En macOS se instalaba como LaunchAgent; en Linux, como servicio systemd. Se auto-eliminaba a las 24 horas.Exfiltraba por triple canal: los datos robados se enviaban al dominio
git-tanstack.com, a nodos de la red descentralizada Session (getsession.org), y a repositorios "dead drop" en GitHub con temática de Dune.
El daño real
No es un ataque teórico. Para cuando npm empezó a remover las versiones maliciosas, el gusano ya se había expandido a más de 169 paquetes con 373 versiones maliciosas. La lista incluye:
- @tanstack/react-router (12.7 millones de descargas semanales)
- @mistralai/mistralai (el SDK oficial de Mistral AI)
- @uipath (40+ paquetes del ecosistema UiPath)
- opensearch-project/opensearch
- guardrails-ai en PyPI
- Decenas de paquetes de datos de aviación, autenticación, agentes MCP y utilidades
El CVE-2026-45321 tiene un CVSS de 9.6 (Crítico). Y es la cuarta ola de una campaña que empezó en septiembre 2025, cada vez más sofisticada. Esta vez lograron lo que ninguna otra había logrado: paquetes maliciosos indistinguibles de los legítimos por firmas criptográficas.
Cinco lecciones duras para desarrolladores
1. pull_request_target es radioactivo. Si tu workflow de CI usa este trigger, asume que cualquier persona en internet puede ejecutar código con los permisos de tu repositorio. La documentación de GitHub lo advierte, pero la advertencia está enterrada. Si necesitas probar código de forks, usa pull_request (sin _target) o un ambiente efímero y aislado. Nunca, bajo ninguna circunstancia, ejecutes código de un fork en un workflow que tenga acceso a secretos o al caché del repositorio base.
2. El caché de CI no es inocente. El caché de GitHub Actions se comparte entre workflows. Un PR malicioso puede envenenarlo. Un push legítimo puede consumirlo. La solución: nunca cachees binarios o dependencias que puedan ser manipuladas desde un fork. Usa claves de caché impredecibles. Y asume que cualquier cosa que un PR externo pueda escribir, un atacante puede controlar.
3. SLSA provenance no es un detector de malware. Las firmas SLSA prueban que el paquete fue construido por quien dice ser. No prueban que el código dentro del paquete sea seguro. Si el pipeline legítimo está comprometido, la firma es perfecta y el paquete es veneno. La provenance es una capa de confianza, no un reemplazo del análisis de seguridad.
4. Tus tokens de desarrollo son las llaves del reino. El ataque no explotó una vulnerabilidad en tu aplicación. Explotó que tenías un token de npm con permisos de publicación en tu CI, un token de GitHub con acceso a repos, y credenciales de cloud en variables de entorno. Usa tokens de npm con scope limitado. Usa OIDC en vez de tokens de larga duración. Rota tus credenciales. Y nunca ejecutes npm install en tu máquina de desarrollo sin pensar qué estás instalando.
5. El postinstall es un punto ciego. Cada vez que instalas un paquete npm, sus scripts de lifecycle (preinstall, postinstall) se ejecutan con los permisos de tu usuario. Este ataque usó optionalDependencies para colarse incluso en instalaciones que no declaraban la dependencia explícitamente. Si tu proyecto no necesita que las dependencias ejecuten scripts, desactívalos: npm config set ignore-scripts true. Si los necesitas, al menos audita qué scripts se ejecutan con npm install --dry-run.
Para empresas: esto no es un problema técnico, es un problema de negocio
Si tu empresa usa JavaScript, TypeScript, Python o cualquier ecosistema con gestores de paquetes, este ataque te afecta aunque no uses TanStack. Las lecciones no son sobre una librería específica — son sobre cómo confiamos en la cadena de suministro de software.
Audita tus dependencias ahora. No la semana que viene. Revisa tus lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) buscando versiones de paquetes comprometidos. Snyk, Socket, Orca y otras herramientas ya tienen las firmas. Si encuentras una versión afectada, asume que ese entorno está comprometido y rota cada secreto al que tuvo acceso.
Aísla tus pipelines de CI. Tus workflows de CI/CD no deberían compartir caché entre PRs externos y pushes internos. Usa entornos separados para forks. Limita los permisos al mínimo indispensable. Si un workflow publica a npm, que sea el único con ese privilegio, y que no ejecute código de fuentes no confiables.
Prepara un playbook de respuesta. Cuando —no si— ocurra el próximo ataque, tu equipo no debería estar googleando qué hacer a las 11 PM. Define un proceso claro: quién decide revocar tokens, quién audita los entornos, quién comunica a clientes. El daemon wiper de este ataque es un recordatorio brutal de que revocar tokens sin antes limpiar el sistema puede ser peor que no hacer nada.
Invierte en higiene de dependencias. Menos dependencias = menos superficie de ataque. Audita regularmente qué paquetes usa tu equipo. Pregúntate si necesitas esa micro-librería de 3 líneas que importa otras 40. Cada dependencia es una decisión de confianza. Trátala como tal.
El elefante en la habitación
El código fuente del gusano Shai-Hulud fue publicado brevemente en GitHub antes de ser removido. Ya existen copias espejo circulando. Esto significa que el ataque no se va a detener aquí — otros actores van a iterar sobre él, como pasó con Mirai.
La cadena de suministro de software es el talón de Aquiles de nuestra industria. Confiamos en que miles de paquetes que no escribimos, mantenidos por personas que no conocemos, construidos en pipelines que no auditamos, van a ejecutarse en nuestros servidores y nuestras laptops sin hacer nada malo. Esa confianza es necesaria para construir software rápido. Pero no debería ser ciega.
La buena noticia es que las defensas existen. La mala es que implementarlas requiere disciplina, no talento especial. Y la disciplina, en seguridad, es lo más escaso.
Top comments (0)