⏱️ Lectura: 14 min

TanStack, una de las suites de librerías más populares del ecosistema React, publicó este 11 de mayo de 2026 un postmortem técnico sobre el compromiso que afectó a 42 paquetes @tanstack/* en npm. En una ventana de apenas seis minutos, un atacante logró empujar 84 versiones maliciosas al registro sin robar un solo token de npm.

📑 En este artículo
  1. TL;DR
  2. Qué pasó con TanStack npm
  3. Anatomía del ataque a TanStack en npm
    1. Paso 1: Pull_request_target y el “Pwn Request”
    2. Paso 2: Envenenamiento de caché entre el fork y la rama main
    3. Paso 3: Extracción del token OIDC en memoria
  4. Línea de tiempo: del fork al detonador
  5. Qué hace el malware una vez ejecutado
  6. Paquetes afectados y familias limpias
  7. Por qué importa: el modelo de “trusted publisher” tiene un punto ciego
  8. Qué hacer si instalaste un paquete @tanstack/* el 11 de mayo
  9. Lecciones para mantenedores de open source en LATAM
  10. Preguntas frecuentes
    1. ¿Mi proyecto está afectado si uso solo @tanstack/react-query?
    2. ¿Robaron tokens de npm de los mantenedores de TanStack?
    3. ¿Cómo sé qué versiones específicas son maliciosas?
    4. ¿Por qué Session/Oxen es tan difícil de bloquear?
    5. ¿Esto pudo haberse detectado antes de los 20 minutos?
    6. ¿Tengo que rotar todas mis credenciales aunque no haya evidencia de exfiltración?
  11. Referencias

El ataque combinó tres técnicas conocidas pero raramente vistas operando en cadena: el patrón “Pwn Request” sobre pull_request_target, envenenamiento de caché en GitHub Actions y extracción en memoria de un token OIDC del propio runner.

TL;DR

  • 84 versiones maliciosas en 42 paquetes @tanstack/* publicadas entre 19:20 y 19:26 UTC del 11 de mayo de 2026.
  • El ataque combinó pull_request_target, cache poisoning en GitHub Actions y robo de token OIDC en memoria.
  • No se robaron tokens npm: el atacante minteó un OIDC token via id-token: write durante el release legítimo.
  • Detectado en 20 minutos por ashishkurmi de StepSecurity; todas las versiones fueron deprecadas.
  • Familias confirmadas como limpias: @tanstack/query*, table*, form*, virtual*, store y start (meta).
  • El payload de 2.3 MB exfiltra credenciales via Session/Oxen messenger, sin C2 bloqueable por IP.
  • Cualquier host que ejecutó npm install con una versión afectada el 11 de mayo debe rotar credenciales AWS, GCP, K8s, Vault, GitHub, npm y SSH.
  • Fork atacante: github.com/zblgg/configuration; commit malicioso 65bf499d con prefijo [skip ci].

Qué pasó con TanStack npm

El equipo de TanStack confirma que, entre las 19:20 y 19:26 UTC del 11 de mayo de 2026, un atacante publicó 84 versiones maliciosas distribuidas en 42 paquetes del namespace @tanstack/* en el registro oficial de npm. Cada paquete recibió aproximadamente dos versiones, publicadas con unos seis minutos de diferencia entre ellas. La detección pública la realizó un investigador externo, ashishkurmi de StepSecurity, en apenas 20 minutos desde la primera publicación. Las versiones fueron deprecadas y npm Security inició la remoción de los tarballs.

Lo más inquietante no es el alcance —considerable, dado el tamaño del namespace— sino el método. El atacante nunca tuvo acceso a una credencial de publicación de TanStack. No robó tokens, no hizo phishing a un mantenedor, no comprometió una cuenta de GitHub. En su lugar, encadenó tres debilidades de la infraestructura de CI/CD para que la propia automatización legítima de la organización publicara los paquetes maliciosos en su nombre.

Diagrama del ataque a TanStack npm con flujo de cache poisoning y robo de OIDC
Las tres piezas que el atacante encadenó para publicar sin tokens propios.

Anatomía del ataque a TanStack en npm

Paso 1: Pull_request_target y el “Pwn Request”

GitHub Actions distingue dos triggers para pull requests. El evento pull_request ejecuta workflows con permisos limitados y exige aprobación manual antes de correr código de un colaborador externo por primera vez. El evento pull_request_target, en cambio, ejecuta con los secretos del repositorio base y bypassa esa aprobación, porque fue diseñado para tareas administrativas como aplicar labels o comentar resultados de benchmarks.

El problema, conocido en la industria de seguridad como “Pwn Request”, aparece cuando un workflow disparado por pull_request_target hace checkout del código de la rama del PR y lo ejecuta. En la práctica eso significa que cualquier fork malicioso puede inyectar código que correrá con los secretos del repositorio base, sin pasar por revisión humana.

En TanStack, los workflows bundle-size.yml y labeler.yml usaban pull_request_target. El primero hacía pnpm install y pnpm nx run @benchmarks/bundle-size:build sobre el merge ref del PR. Eso fue todo lo que el atacante necesitó: con sólo abrir un PR, su payload se ejecutaba en un runner con permisos del repo base.

Paso 2: Envenenamiento de caché entre el fork y la rama main

La GitHub Actions Cache mantiene entradas separadas por scope, normalmente la rama. Pero la documentación oficial reconoce una excepción: los workflows que corren para un pull request pueden escribir a la caché del repositorio base bajo ciertos refs. El malware aprovechó exactamente esto.

Durante el run de bundle-size.yml, el payload bundleado en packages/history/vite_setup.mjs —un archivo JS de unas 30,000 líneas— se ejecutó. Su tarea no fue exfiltrar nada todavía, sino guardar una entrada de caché de 1.1 GB con la clave Linux-pnpm-store-... apuntando al scope refs/heads/main. Esa clave coincidía exactamente con la que release.yml, el workflow real de publicación, buscaría más tarde al correr sobre main.

Después de envenenar la caché, el atacante hizo force-push al PR llevándolo de vuelta al HEAD de main: un diff de cero archivos. Cerró el PR. La evidencia visual del ataque desapareció. La caché envenenada, no.

⚠️ Ojo: Si tu pipeline de release hace restore de caché producida por workflows disparados desde forks, asumí que esa caché es código no confiable. Tratala como input adversarial, no como artefacto reproducible.

Paso 3: Extracción del token OIDC en memoria

Aquí es donde el ataque deja de ser un patrón conocido y pasa a ser una pieza de ingeniería más sofisticada. TanStack usa la modalidad de “trusted publisher” de npm, vinculada por OIDC a TanStack/router release.yml@refs/heads/main. Esto elimina la necesidad de almacenar un NPM_TOKEN: el runner solicita un OIDC token a GitHub durante el workflow, npm lo verifica contra la binding y permite publicar.

Cuando alguien mergeó un PR legítimo no relacionado, release.yml arrancó. El workflow hizo restore de la caché —y obtuvo la versión envenenada—. Los tests fallaron por modificaciones del malware, por lo que el paso Publish Packages nunca corrió. Pero el malware había inyectado código en la fase de teardown del workflow. Como release.yml declaraba permissions: id-token: write, el runner había recibido un OIDC token. El malware lo extrajo de la memoria del proceso de Actions y disparó publicaciones HTTP directas a registry.npmjs.org con ese token. npm vio una request legítimamente firmada por el workflow autorizado y aceptó cada publish.

Línea de tiempo: del fork al detonador

El postmortem documenta la cronología con precisión forense. El flujo completo se puede visualizar así:

flowchart TD
    A["Atacante crea fork malicioso 10 may 17:16"] --> B["Commit malicioso con skip ci"]
    B --> C["Abre PR #7378 contra TanStack router"]
    C --> D["pull_request_target ejecuta workflows"]
    D --> E["pnpm install corre vite_setup.mjs"]
    E --> F["Cache envenenada de 1.1 GB persistida"]
    F --> G["Atacante cierra el PR y borra rama"]
    G --> H["8 horas despues merge legitimo en main"]
    H --> I["release.yml restaura cache envenenada"]
    I --> J["Malware mintea OIDC token en memoria"]
    J --> K["Publica 84 versiones en 42 paquetes"]
  • 10 de mayo, 17:16 UTC: el atacante crea el fork github.com/zblgg/configuration (renombrado deliberadamente para evadir búsquedas).
  • 10 de mayo, 23:29 UTC: commit malicioso 65bf499d con identidad falsa claude <[email protected]> y prefijo [skip ci] en el mensaje para suprimir CI.
  • 11 de mayo, ~10:49 UTC: PR #7378 “WIP: simplify history build” abierto contra main.
  • 11 de mayo, 10:49 – 11:11 UTC: múltiples force-pushes; cada uno dispara bundle-size.yml. El último incluye el commit malicioso.
  • 11 de mayo, 11:29 UTC: caché envenenada de 1.1 GB persistida en el scope de main.
  • 11 de mayo, 11:31 UTC: el atacante limpia el PR a un diff vacío y lo cierra. La rama se elimina.
  • 11 de mayo, 19:15 UTC: un merge legítimo (PR #7369) dispara release.yml.
  • 11 de mayo, 19:20 UTC en adelante: comienzan las publicaciones maliciosas a registry.npmjs.org.
  • 11 de mayo, ~19:40 UTC: ashishkurmi (StepSecurity) reporta públicamente las versiones maliciosas.

Casi nueve horas separaron la siembra de la cosecha. Durante ese tiempo, ningún humano podía ver evidencia del ataque en el repositorio: el PR estaba cerrado, la rama borrada, los commits maliciosos sólo existían en el fork ahora invisible para búsquedas convencionales.

Qué hace el malware una vez ejecutado

El payload final, router_init.js, pesa unos 2.3 MB y está fuertemente ofuscado. Cuando una víctima corre npm install, pnpm install o yarn install contra una versión afectada, npm resuelve una entrada optionalDependencies que apunta a un commit huérfano en el fork. El script prepare se ejecuta como parte del ciclo de vida estándar de npm.

A partir de ahí, el malware:

  • Recolecta credenciales de ubicaciones comunes: IMDS de AWS, Secrets Manager, metadata de GCP, tokens de service accounts de Kubernetes, tokens de Vault, ~/.npmrc, tokens de GitHub (variables de entorno, CLI gh, .git-credentials), llaves SSH privadas.
  • Exfiltra todo lo recolectado a través de la red de mensajería Session/Oxen (filev2.getsession.org, seed1-3.getsession.org). Como Session es end-to-end encrypted sin servidor de C2 controlado por el atacante, bloquear por IP o dominio es la única mitigación de red posible.
  • Se auto-propaga: enumera otros paquetes que la víctima mantiene consultando registry.npmjs.org/-/v1/search?text=maintainer:<user> y los republica con la misma inyección si tiene los tokens necesarios.

Una invocación rápida que cualquier mantenedor puede correr para detectar versiones instaladas en su árbol de dependencias:

# Linux / macOS
npm ls --all 2>/dev/null | grep -E "@tanstack/(router|db|ai|pacer|devtools|cli|hotkeys|intent)"
pnpm why @tanstack/router 2>/dev/null

# Windows (PowerShell)
npm ls --all | Select-String "@tanstack/(router|db|ai|pacer|devtools|cli|hotkeys|intent)"
pnpm why "@tanstack/router"
Auditoria de logs npm tras compromiso de TanStack en cadena de suministro
Auditoría obligatoria: rotar credenciales y revisar logs de IMDS, npm publish y GitHub.

Paquetes afectados y familias limpias

42 paquetes recibieron versiones maliciosas, principalmente del lado de @tanstack/router*, @tanstack/db*, @tanstack/ai*, @tanstack/pacer*, @tanstack/start-* (los modulares, no el meta), @tanstack/devtools*, entre otros. El issue de tracking #7383 incluye la tabla completa con cada versión afectada y su timestamp de publicación.

Confirmadas como limpias —no fueron tocadas durante la ventana del incidente—:

  • @tanstack/query*
  • @tanstack/table*
  • @tanstack/form*
  • @tanstack/virtual*
  • @tanstack/store
  • @tanstack/start (el meta-paquete, no @tanstack/start-*)

Si tu proyecto en LATAM usa Query, Table, Form o Virtual exclusivamente, no fuiste afectado por esta ola. La distinción entre start (meta) y start-* (modulares) es importante: el meta-paquete no recibió versiones maliciosas, pero los paquetes específicos que él agrupa transitivamente sí pueden haberlas recibido.

💡 Tip: Antes de correr npm install mañana, agregá --ignore-scripts como flag temporal en tu CI y en tu shell local. Esto previene la ejecución del script prepare que detona el payload, aunque rompe paquetes que dependen legítimamente de scripts de build.

Por qué importa: el modelo de “trusted publisher” tiene un punto ciego

OIDC para publicaciones —respaldado por npm, PyPI y otros— resuelve el problema clásico del token compartido. Pero el postmortem de TanStack expone una asunción rota: confiar en que sólo los pasos declarados del workflow usarán el token. Si el atacante consigue ejecutar cualquier código en el runner mientras id-token: write está activo —y un workflow con permisos elevados es exactamente el blanco más atractivo— puede mintear y usar el token sin pasar por los pasos legítimos.

Esto convierte la combinación “pull_request_target + cache de Actions compartida con main + workflow de publish con OIDC” en una bomba de tiempo. El postmortem llama a tratar la GitHub Actions Cache como territorio hostil tan pronto como un PR de un fork puede escribir en ella.

Qué hacer si instalaste un paquete @tanstack/* el 11 de mayo

  1. Identificar si la versión instalada está en la lista de afectadas (revisar el GHSA enlazado en Referencias).
  2. Considerar el host como comprometido. No es paranoia: el malware corre con tus permisos.
  3. Rotar credenciales: AWS, GCP, Kubernetes, Vault, npm, GitHub PAT y deploy keys, llaves SSH, cualquier secreto guardado en el host.
  4. Auditar logs de los proveedores cloud por accesos anómalos desde IPs no habituales en las últimas 24 horas.
  5. Correr pnpm store prune, borrar ~/.npm y node_modules, y reinstalar contra versiones limpias una vez deprecadas las maliciosas.
  6. Si trabajás en una organización, notificar al equipo de seguridad y considerar invalidar tokens de sesión de servicios donde el host tenga acceso.

Lecciones para mantenedores de open source en LATAM

El postmortem recomienda explícitamente —y vale para cualquier proyecto que publique paquetes desde GitHub Actions—:

  • Migrar workflows que necesiten secretos de PRs a flujos con pull_request y aprobación manual. Si necesitás pull_request_target, evitá hacer checkout del código del PR.
  • No usar la GitHub Actions Cache compartida entre forks y main para builds reproducibles que luego firmen o publiquen artefactos.
  • Restringir permissions: id-token: write al mínimo absoluto de pasos. Idealmente, sólo al step que publica.
  • Considerar aislar la publicación en un repositorio dedicado, sin checkout de código de PRs externos.
  • Auditar la lista de workflows con pull_request_target en todos los repos públicos. StepSecurity y otras herramientas ofrecen escaneo automático.
  • Loguear y monitorear publicaciones a npm en tiempo real con webhooks o herramientas como Socket: la diferencia entre 20 minutos y 2 horas de detección puede ser miles de hosts comprometidos.
📌 Nota: El nombre del fork —zblgg/configuration— estaba elegido para no aparecer cuando alguien buscara forks de TanStack/router en la UI de GitHub. Esta pequeña capa de stealth indica un atacante familiarizado con cómo los equipos auditan su superficie de exposición.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Mi proyecto está afectado si uso solo @tanstack/react-query?

No. La familia @tanstack/query* está confirmada como limpia. El incidente afectó principalmente a paquetes del lado de Router, DB, AI, Pacer, Devtools y CLI.

¿Robaron tokens de npm de los mantenedores de TanStack?

No. El postmortem es explícito en que ninguna credencial de publicación fue comprometida. El atacante minteó un token OIDC en el runner de GitHub Actions durante un workflow legítimo y lo usó para publicar desde dentro del proceso.

¿Cómo sé qué versiones específicas son maliciosas?

El issue de tracking TanStack/router#7383 y el GitHub Security Advisory GHSA-g7cv-rxg3-hmpx listan cada paquete y versión afectada con la fecha y hora exactas de publicación.

¿Por qué Session/Oxen es tan difícil de bloquear?

Session es un protocolo de mensajería descentralizado encriptado end-to-end. Las exfiltraciones no atraviesan un servidor controlado por el atacante: pasan por la red Oxen y son recogidas más tarde por el destinatario. No hay IP “del atacante” que bloquear; bloquear filev2.getsession.org y los seeds en el firewall es la única mitigación de red disponible.

¿Esto pudo haberse detectado antes de los 20 minutos?

Sí. Detección en tiempo real de publicaciones a npm con tarballs anómalos por tamaño o spike de versiones es factible con herramientas como Socket, StepSecurity o las nuevas alertas de npm. La velocidad de detección dependió de que un investigador externo estuviera vigilando, no de la infraestructura de TanStack.

¿Tengo que rotar todas mis credenciales aunque no haya evidencia de exfiltración?

Sí. La regla en supply-chain attacks es asumir lo peor: si el malware corrió como tu usuario, todo lo que ese usuario podía leer es asumible como exfiltrado. La auditoría de logs llega después, no antes, de la rotación de secretos.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Categorías: Noticias Tech

Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.