Mini Shai-Hulud: The TanStack Supply Chain Attack That Defeated SLSA Provenance

Cómo el ataque Mini Shai-Hulud envenenó paquetes TanStack npm y superó las garantías de procedencia de SLSA Level 3.

Mini Shai-Hulud: The TanStack Supply Chain Attack That Defeated SLSA Provenance
📌
Seguridad de la cadena de suministroColibriSec Knowledge Base

El 11 de mayo de 2026, entre las 19:20 y las 19:26 UTC (una ventana de seis minutos y 35 segundos), un atacante publicó 84 versiones maliciosas en 42 paquetes @tanstack/* en npm. TanStack Router, TanStack Start y una constelación de paquetes relacionados descargados millones de veces por semana se vieron comprometidos. No se robaron credenciales de npm. Ninguna cuenta de mantenedor fue objeto de phishing. Todos los miembros del equipo tenían 2FA habilitado. Los paquetes pasaron los controles de procedencia de la SLSA y portaban certificados firmados válidos. Nada de eso importaó.

Esta es la historia de Mini Shai-Hulud Wave 4: el ataque a la cadena de suministro de npm más sofisticado jamás documentado, atribuido a TeamPCP, el mismo grupo detrás del compromiso Trivy en marzo y el ataque CLI de Bitwarden en abril. Si instaló algún paquete @tanstack afectado el 11 de mayo de 2026, su máquina puede estar comprometida en este momento.

¿Quién es TanStack y por qué es importante?

TanStack es una de las suites de código abierto más utilizadas en el ecosistema. TanStack Query solo tiene más de 45 millones de descargas semanales. TanStack Router, el objetivo principal de este ataque, es la biblioteca de enrutamiento estándar para TanStack Start, un marco de reacción completo con una adopción de rápido crecimiento. Los paquetes afectados en este ataque representan colectivamente decenas de millones de instalaciones semanales a través de aplicaciones React, Vue y SolidJS.

Cuando una biblioteca en esta escala está comprometida, el radio de explosión no se mide en desarrolladores individuales — se mide en cada oleoducto CI/CD, cada ordenador portátil desarrollador, y cada servidor de producción que corrió npm instalar en cualquiera de los 42 paquetes afectados durante la ventana de seis minutos.

El ataque: tres vulnerabilidades encadenadas

DESCRIPCIÓN GENERAL DE LA CADENA DE ATAQUE

Phase 1 (May 10)  → Fork createdMalicious PR opened (pull_request_target)1.1 GB cache poisonedPR closed, traces removed
Phase 2 (May 11 19:15) → Legit PR merged, release.yml firesPoisoned cache restoredToken OIDC extraído de /proc/mem84 malicious packages published in 6m 35s
Phase 3 (19:20→)    → Credentials harvested (100+ paths)npm packages enumeratedWorm republishes to 160+ packagesrm -rf ~/ if token revoked

Ventana total de publicación: 6 minutos 35 segundos tención Detección: ~30 minutos después de la primera publicación TEN Detector: ashishkurmi / StepSecurity

El compromiso de TanStack no fue una sola explotación. Era una cadena de tres etapas, cada paso que se basaba en el último, explotando supuestos de confianza horneados en el ecosistema de GitHub Actions. TeamPCP nombró esta cadena de herramientas Mini Shai-Hulud —después de la tormenta de Frank Herbert's Dune— adecuada para un gusano que se propaga por el ecosistema de las npm que consume todo en su camino.

Etapa 1 - La Pwn Solicitud: pull request target Abuse

El ataque comenzó el 10 de mayo de 2026 cuando el atacante creó un tenedor de TanStack/router bajo la cuenta zblgg, renombrando deliberadamente a zblgg/configuration para evadir búsquedas de la lista de tenedor. A malicious commit was then authored under the fabricated identity [email protected] — a deliberate choice designed to implicate AI tooling and create noise during forensic investigation.

El exploit clave fue un flujo de trabajo de GitHub Actions llamado paquete-size.yml. Este flujo de trabajo utilizó el disparador pull request target —que funciona en el contexto del repositorio base en lugar del tenedor, dándole acceso a secretos de repositorio y caché. El flujo de trabajo estaba destinado a los tamaños de los paquetes de referencia para la entrada de PRs de los tenedores.

El autor del flujo de trabajo intentó imponer un límite de confianza y señaló en los comentarios la intención de mantener el trabajo de evaluación comparativa como de solitario lectura. Este intento falló por dos razones no obvias:

on:
  pull_request_target:
    paths: ['packages/**', 'benchmarks/**']

jobs:
  benchmark-pr:
    steps:
      - uses: actions/[email protected]
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge  # fork code!

      - uses: TanStack/config/.github/setup@main  # calls actions/cache@v5

      - run: pnpm nx run @benchmarks/bundle-size:build  # executes fork-controlled code

El paso de checkout extrae el código fusionado del tenedor, lo que significa que JavaScript controlado por el atacante corre dentro del contexto de CI confiable del repositorio base. Fijar permisos: contenido: leer no era suficiente protección — y aquí es por qué.

Etapa 2 — Cache Poisoning Across the Trust Boundary

GitHub Actions cache está en alcance por repositorio y se comparte a través de todos los disparadores de flujo de trabajo en ese repositorio, incluyendo pull request target se ejecuta de fork PRs y carreras de producción push-to-main. Un malentendido crítico causó que el fideicomiso se dividiera: acciones/cache@v5's post-job save utiliza un token de corredor interno, no GITHUB TOKEN del flujo de trabajo. Los permisos: campo solo controla el alcance GITHUB TOKEN. Cache escribe no está bloqueado por él.

El malicioso vite setup.mjs del atacante — una carga de pago de JavaScript de 30.000 líneas— funcionó durante el flujo de trabajo de referencia y escribió una entrada de caché envenenada de 1.1 GB, clave para coincidir exactamente con lo que el flujo de trabajo de la versión.yml buscaría el siguiente empuje a la base. Clave de caché: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11.

Después de que el caché fue escrito, el atacante impulsó el PR de nuevo al HEAD principal actual — haciendo que el PR visible un no-op 0-file — entonces lo cerró y borró la rama. La entrada de caché se mantuvo. Esperando.

Etapa 3 — OIDC Token Extracción de Runner Memory

El 11 de mayo a las 19:15 UTC, un mantenedor legítimo fusiónó una corrección de CSS no relacionada. Se activó el flujo de trabajo release.yml. Restauró el caché planeadodo de 1,1 GB. El malware se eyecutó dentro del contexto de lanzamiento confiable.

La carga de pago localizó el GitHub Actions Runner. El proceso de trabajo mediante /proc/cantapid título/maps y /proc/cantapid confianza/mem, extrajo el token OIDC de la memoria del proceso del corredor, y lo usó para autenticar directamente a las npm como la unión de editor de confianza TanStack/router.yml@refs/heads/main.

Los paquetes maliciosos fueron publicados con certificados válidos SLSA Build Level 3, el primer ataque de cadena de suministro de npm documentado para producir paquetes maliciosos validamente certificados. Cada herramienta de verificación de la procedencia dio a estos paquetes una factura limpia de salud. El flujo de trabajo en sí mismo mostró como fracasado. El malware ya había corrido.

El malware: lo que Mini Shai-Hulud le hace a su máquina

FLUJO DE EJECUCIÓN DE MALWARE: activado por el enlace del ciclo de vida de instalación de npm

1. HARVEST

~/.aws/credentials
~/.config/gcloud/
~/.kube/config
~/.vault-token
GITHUB TOKEN
~/.npmrc tokens
~/.ssh/* claves
Docker config
Credos Terraform
Créditos del Código Claude
OpenAI / teclas antropópicas
Carteras Crypto
100+ objetivos de archivo

2. PERSIST

sistemad servicio de usuario
cron entry (Linux)
Lista de agentes de lanzamiento (Mac)
Claude Code ganchos
VS Code extension
- Inyección básica
~/.zshrc injection
~/.inyección de perfiles

Encuestas GitHub token
cada 60 segundos


¿Token revocó?
→ rm -rf ~/

3. EXFILTRATE

Primaria:
Protocolo del período de sesiones CDN
filev2.getsession.org
E2E encriptado, no C2
bloqueo IP inútil

Secundaria:
GitHub GraphQL API
Dead-drop commits
[email protected]
Nombres de rama dependabot
de novelas Dune

4. PROPAGADO

Registro de consultas npm
para todos los paquetes
mantenimiento propio

Re-publish each with
pago malicioso
usando credos robados

Cascaded to:
UiPath (80+ pkgs)
Mistral AI
DraftLab, OpenSearch
Total de paquetes 160+

La carga útil es router init.js — un archivo JavaScript muy obfuscado de 2.3 MB. Una vez ejecutado durante las npm instalar ganchos de ciclo de vida (a través de un gancho de preparación en una dependencia opcional maliciosa @tanstack/setup), se ejecuta la siguiente secuencia.

Cosecha temporal - 100+ Metas

Cloud:
  AWS:        ~/.aws/credentials, ~/.aws/config, IMDS, Secrets Manager
  GCP:        ~/.config/gcloud/, metadata endpoint
  Azure:      ~/.azure/
  Kubernetes: ~/.kube/config, /var/run/secrets/kubernetes.io/serviceaccount/token
  Vault:      ~/.vault-token, VAULT_TOKEN env

Developer tools:
  GitHub:     GITHUB_TOKEN env, ~/.config/gh/hosts.yml, .git-credentials
  npm:        ~/.npmrc
  SSH:        ~/.ssh/ (all private keys)
  Docker:     ~/.docker/config.json
  Terraform:  ~/.terraform.d/credentials.tfrc.json

AI tooling:
  Claude Code: ~/.claude/credentials.json
  OpenAI:     OPENAI_API_KEY env
  Anthropic:  ANTHROPIC_API_KEY env

Crypto wallets, IDE configs, shell history, browser credentials

Persistencia — Survives reinicios

El malware instala persistencia a través de múltiples vectores: un servicio de usuario sistematizado y entradas de cron en Linux, una plist de LaunchAgent en macOS. También inyecta ganchos en los archivos Claude Code, VS Code y shell RC (.bashrc, .zshrc, .profile). Un daemon de votación corre cada 60 segundos viendo el estado de la ficha GitHub de la víctima.

Exfiltración - Diseñado para el bloqueo de Evade

Canal primario: Protocolo de sesión CDN (filev2.getsession.org, seed1/2/3.getsession.org) — final a extremo cifrado, ningún bloqueo IP controlado por atacantes es en gran medida ineficaz. Canal secundario: la propia API de GraphQL de GitHub, donde se cometen errores mortales se autorizan como [email protected] con nombres de ramas de estilo Dependabot extraídos del universo Dune de Frank Herbert.

Autopropagación: Esto es un gusano

Después de cosechar credenciales, el malware llama a la API de registro npm para enumerar cada paquete que la víctima mantiene. Luego republica cada uno con la carga maliciosa inyectada, utilizando las credenciales de npm robadas. Cada desarrollador infectado infecta toda su cartera de paquetes, que infecta a cada otro desarrollador que instala esos paquetes, que infecta sus carteras, etc. Así es como la infección se diseminó a UiPath, Mistral AI, DraftLab, OpenSearch y 160 paquetes adicionales dentro de horas.

La carga destructiva: rm -rf ~/

El elemento más peligroso no es el robo credencial, es lo que sucede cuando intentas remediar. El daemon de votación monitorea que se revocará la señal GitHub de la víctima. Cuando detecta la revocación, ejecuta rm -rf ~/. Todo tu directorio de inicio está borrado. TeamPCP deliberadamente armó instintos estándar de respuesta a incidentes. La secuencia de remediación a continuación debe ser seguida exactamente.

Lista de paquetes (envases clave)

42 paquetes en 84 versiones. @tanstack/react-router (1.169.5, 1.169.8), @tanstack/router-core (1.169.5, 1.169.8), @tanstack/solid-router (1.169.5, 1.169.8), @tanstack/vue-router (1.169.5, 1.169.8), @tanstack/react-start (1.167, 1.167.68, 1.167. Difusión secundaria: @mistralai/mistralai (2.2.3, 2.2.4), @opensearch-project/opensearch (3.6.2), decenas de paquetes @uipath/*.

Confirmado limpio (no afectado): @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store.

Cómo saber si usted está afectado

# Check lockfile for affected versions
grep -E '@tanstack/(react-router|router-core|solid-router|vue-router|react-start|router-plugin|start-server-core)' \
  package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null

# Check for running malware processes
ps aux | grep -E 'tanstack_runner|router_init'
crontab -l | grep tanstack
ls ~/.config/systemd/user/ | grep tanstack
ls ~/Library/LaunchAgents/ | grep tanstack   # macOS

Remediación - Orden crítica de operaciones

NO revoque su token GitHub primero. El malware relojes para esto y ejecutar inmediatamente rm -rf ~/. Mata al demonio antes de revocar cualquier cosa.

Paso 1 - Matar al Daemon

# Kill malware processes immediately
pkill -f tanstack_runner 2>/dev/null
pkill -f router_init 2>/dev/null

# Remove persistence — Linux
systemctl --user stop tanstack.service 2>/dev/null
systemctl --user disable tanstack.service 2>/dev/null
rm -f ~/.config/systemd/user/tanstack.service
crontab -l | grep -v tanstack | crontab -

# Remove persistence — macOS
launchctl unload ~/Library/LaunchAgents/com.tanstack.runner.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.tanstack.runner.plist

# Remove shell hooks
sed -i '/tanstack/d' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null

Paso 2 - Rotar todas las credenciales (de una máquina limpia)

Rotación: GitHub fichas de acceso personal y aplicaciones OAuth, fichas de acceso npm, credenciales de AWS (IAM), claves de cuenta de servicio GCP, fichas de cuenta de servicio Kubernetes, fichas Vault, claves privadas SSH (generar nuevos pares, actualizaciones autorizadas keys en todas partes), fichas de Docker Hub, cualquier clave de API en el host afectado, contenidos de ~/crer.

Paso 3 - Compruebe si usted esparce el gusano

# If you maintain npm packages, audit your publish history for May 11
npm search --json maintainer:<your-npm-username> | jq '.[].name' | \
  xargs -I{} npm view {} time --json | jq 'to_entries[] | select(.key | startswith("1."))'

# Any versions published on 2026-05-11 that you did not explicitly publish
# should be treated as compromised and deprecated immediately

Mitigations: Protecting Your Pipelines

1. Nunca ejecutar código de tenedor con pull request target

# UNSAFE: runs fork code in base repo context with cache access
on:
  pull_request_target:
steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}  # fork code

# SAFE: separate workflow, no base-repo privileges
on:
  pull_request:
steps:
  - uses: actions/checkout@v4  # checks out fork safely, no cache poisoning

2. Pin all Actions to commit SHAs

# Vulnerable to tag-moving attacks
- uses: actions/checkout@v4
- uses: actions/cache@v5

# Immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684

3. Utilice mínimoReleaseAge en su administrador de paquetes

Las organizaciones con mínimoReleaseAge configuradas fueron protegidas automáticamente; los paquetes comprometidos fueron deprecados antes de que expirara el retraso del envejecimiento. Este único control habría bloqueado el ataque para la mayoría de los usuarios.

# Add to .npmrc or .pnpmrc
minimumReleaseAge=24h

4. Restringir id-token: escribir al trabajo publicado específico

jobs:
  publish:
    permissions:
      id-token: write  # only this job can mint OIDC tokens
      contents: read
  test:
    permissions:
      contents: read   # no OIDC access

5. Añadir Harden-Runner a tus flujos de trabajo

steps:
  - uses: step-security/harden-runner@v2
    with:
      egress-policy: audit  # detects unexpected outbound traffic
      # Use 'block' to prevent exfiltration entirely

6. Vigilar su npm publicar historia

Configurar alerta para cualquier nueva versión publicada bajo tu cuenta Npm. Cualquier publicación que no haya activado explícitamente debe ser investigada inmediatamente. npm admite notificaciones de correo electrónico y eventos webhook para publicar acciones.

¿Por qué la venganza SLSA se desvaneció y qué significa esto?

SLSA Build Level 3 es el estándar de oro actual para la seguridad de la cadena de suministro. TanStack lo había aplicado plenamente. El ataque Mini Shai-Hulud lo derrotó por completo. El malware corrió dentro del contexto de flujo de trabajo de lanzamiento legítimo.yml, utilizando un token OIDC robado emitido a TanStack/router release.yml@refs/heads/main — así que cada prueba de procedencia que generó fue criptográficamente válida.

Esto expone una suposición fundamental en el modelo de amenaza SLSA: que el entorno CI que dirige la construcción es digno de confianza. Cuando ese ambiente se ve comprometido a través del envenenamiento por caché, el certificado certifica lo incorrecto. La venganza muestra dónde y cómo funcionaba una construcción — no puede verificar que el medio ambiente estuviera limpio. Wave 4 es el primer ataque para demostrar esta brecha en la práctica.

Esto no significa que la procedencia sea inútil — significa que la procedencia debe estar emparejada con el aislamiento del entorno de la CI, el monitoreo del egreso y la detección del amortiguador en el entorno de construcción en sí.

El patrón: campaña de escalada de TeamPCP

TeamPCP, también rastreado como DeadCatx3, PCPcat, ShellForce y CipherForce, ha estado ejecutando Mini Shai-Hulud desde septiembre de 2025. Cada onda ha aumentado en la sofisticación técnica. La onda 4 no es notable para escala, sino para el bypass de la procedencia, una capacidad que no había demostrado ningún ataque previo a la cadena de suministro.

El hilo que conecta Trivy (marzo 2026), Bitwarden CLI (abril 2026), y ahora TanStack (mayo 2026) es consistente: explotar tuberías de confianza CI/CD en lugar de robar credenciales directamente, utilizar los propios mecanismos de confianza del ecosistema contra sí mismo. La campaña continúa. Se siguen descubriendo paquetes adicionales a través de la propagación del gusano.


Referencias: TanStack Official Postmortem ← PasoSecurity Mini Shai-Hulud analysis  OpenAI incident response ← GitHub Security Advisory GHSA-g7cv-rxg3-hmpx Silencio Snyk TanSolicitar asesoría en seguridad ← Orca Análisis de seguridad TEN Strobes desglose técnico ANTE