El 28 de agosto de 2026 fue comprometido el paquete npm @7nohe/openapi-react-query-codegen, una herramienta que genera clientes TypeScript y hooks de TanStack Query a partir de esquemas OpenAPI.[1][2][3]
El atacante no necesitó la contraseña npm del maintainer ni un token de publicación de larga duración.
La ruta de entrada estaba en el workflow de release de GitHub Actions.
El repositorio escuchaba eventos:
issue_comment
y un comentario en un pull request con el texto exacto:
npm publish
podía activar el flujo de publicación sin validar correctamente si quien comentaba era maintainer, collaborator o un usuario externo.[1][2]
Después el pipeline:
hacía checkout del código del PR
→ ejecutaba pnpm install
→ tenía id-token: write
→ publicaba mediante npm Trusted Publishing
El resultado fue especialmente relevante: 10 versiones maliciosas se publicaron con npm provenance válida.[1][3]
La lección principal:
La provenance puede demostrar correctamente el origen de un build y, aun así, el build puede ser malicioso.
También hay que corregir un titular engañoso: @tanstack/react-query no fue comprometido en este incidente. El paquete atacado es un generador de código independiente utilizado con TanStack Query.[6][7]
Estado de la información: 31 de agosto de 2026.
TL;DR
| Pregunta | Respuesta verificada |
|---|---|
| Paquete comprometido | @7nohe/openapi-react-query-codegen |
| Fecha | 28 de agosto de 2026 |
| Versiones maliciosas | 10 |
| Versiones estables maliciosas | 8 |
| Prereleases maliciosas | 2 |
¿@tanstack/react-query comprometido? | No |
| Vector principal | Workflow GitHub Actions issue_comment mal configurado |
| ¿Contraseña de maintainer necesaria? | No |
| ¿Long-lived npm token necesario? | No |
| Autoridad de publicación | id-token: write + npm Trusted Publishing |
| ¿Provenance válida? | Sí |
| ¿Provenance = código seguro? | No |
| Rutas de ejecución | binding.gyp y, en algunas versiones, preinstall |
| Payload principal | 3FWCvzduYZg.js |
latest actual | 3.0.2 |
| Severidad | Critical, CVSS 9.6 |
| CVE | Sin CVE conocido en el advisory al 31 de agosto |
| Familia | Actividad vinculada/derivada de Mini Shai-Hulud |
| ¿Atribución del operador segura? | No |
| Respuesta prioritaria | Aislar host y rotar credenciales desde una máquina limpia |
¿Qué ocurrió el 28 de agosto?
El advisory GHSA-9pvf-vcx3-x239 enumera 10 versiones maliciosas publicadas aproximadamente entre 20:00 y 20:21 UTC.[1]
Estables:
0.5.4
0.5.5
1.6.3
1.6.4
2.2.1
2.2.2
3.0.3
3.0.4
Prereleases:
0.0.0-365d4eb738d3146583431948d3ba6e27a32556be
0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab
El tag latest apuntó a 3.0.4 desde aproximadamente 20:19:29 UTC hasta cerca de 22:51 UTC, cuando volvió a 3.0.2.[1]
TanStack Query no fue comprometido
Decir:
TanStack Query hacked
es inexacto.
@7nohe/openapi-react-query-codegen es un proyecto independiente que genera código para:
@tanstack/react-query
No es un paquete oficial TanStack.
Además, el postmortem de TanStack sobre el incidente de mayo también indicó explícitamente que Query no estuvo afectado.[11]
¿Qué hace el paquete?
Genera:
- clientes API tipados,
- query keys,
queryOptions,infiniteQueryOptions,useQuery,useMutation,- helpers de suspense,
- prefetch y SSR.
Flujo típico:
OpenAPI schema
↓
codegen
↓
cliente TypeScript
↓
hooks/options de TanStack Query
¿Qué tan usado era?
El 31 de agosto npm mostraba alrededor de 155.000 descargas semanales.[6]
La cifra es dinámica.
Lo importante es que se ejecuta en laptops de desarrolladores y runners CI, entornos donde suelen existir credenciales GitHub, SSH, cloud y deployment.
El ataque comenzó en GitHub Actions
El workflow respondía a:
on:
issue_comment:
types: [created]
La cadena peligrosa fue:
comentario externo
+ sin autorización del actor
+ checkout del PR
+ ejecución del código
+ id-token: write
+ publish
Un comentario podía entrar en el release
Se comprobaba básicamente que el comentario perteneciera a un PR y fuera exactamente npm publish.[1][2]
Faltaba un gate fiable para roles como:
OWNER
MEMBER
COLLABORATOR
Un usuario externo podía alcanzar así el job privilegiado.
El checkout convirtió input en ejecución
El workflow hacía checkout del código del PR y después:
pnpm install
El contenido controlado por el atacante se ejecutaba dentro de un contexto de release privilegiado.
GitHub desaconseja ejecutar código no confiable de pull requests en workflows con privilegios.[9]
id-token: write habilitó Trusted Publishing
El job tenía:
permissions:
id-token: write
Es una configuración legítima para npm Trusted Publishing vía OIDC.[10]
El problema fue permitir que código no confiable se ejecutara en un job que podía obtener esa identidad.
No fue necesario robar una contraseña
StepSecurity destaca que no era necesario robar una contraseña npm ni un publish token de larga duración.[2]
Modelo clásico:
robar token → publicar
Modelo actual:
controlar ejecución del workflow confiable
→ obtener identidad legítima
→ publicar
Las versiones maliciosas tenían provenance válida
Las publicaciones pasaron por el workflow real y Trusted Publishing, por lo que tenían provenance válida.[1][3]
La provenance afirmaba correctamente:
este artefacto viene
de este repositorio
mediante este workflow
Pero no garantizaba que el código del PR estuviera autorizado.
Provenance no es detección de malware
Puede probar:
- origen del build,
- identidad del workflow,
- relación con el source,
- cadena de custodia.
No prueba automáticamente:
- seguridad del source,
- autorización del PR,
- seguridad lógica del workflow,
- ausencia de malware.
Por tanto:
valid provenance ≠ safe package
Primera ruta de ejecución: binding.gyp
Algunas versiones no tenían un preinstall visible.[1]
En su lugar añadían:
binding.gyp
Se abusó del evaluador Python de GYP para alcanzar os.system() y ejecutar el payload JavaScript.[1][5]
Buscar únicamente lifecycle scripts no es suficiente.
Segunda ruta: preinstall
Otras versiones estables añadieron:
"preinstall": "node 3FWCvzduYZg.js"
Fueron:
0.5.5
1.6.4
2.2.2
3.0.4
Las primeras versiones estables usaban binding.gyp sin un lifecycle script explícito.[1]
Las dos prereleases eran distintas
Las dos versiones 0.0.0-* tenían un preinstall malicioso, pero:
"files": ["dist"]
dejó fuera los archivos payload del tarball.[1]
Las 10 publicaciones son maliciosas, aunque no sean artefactos técnicamente idénticos.
Payload principal: 3FWCvzduYZg.js
Los tarballs estables incluían:
3FWCvzduYZg.js
de aproximadamente 4,4 a 6,4 MB.[1]
StepSecurity observó que 0.5.4 pasó de unos 41 KB en 0.5.3 a más de 5,6 MB.[2]
Un salto de tamaño así es una anomalía útil para detección.
Comportamiento en runtime
StepSecurity ejecutó las versiones en runners aislados.[2]
Se observó:
Node inicia payload
→ curl descarga Bun
→ Bun se ejecuta desde /tmp/trinnyyyy-*
→ gh auth token
→ Git Credential Manager
→ comprobación ssh/scp
→ enumeración de procesos
→ updater.py temporal
También hubo acceso a GitHub API y sondeo del metadata hostname de Google Cloud.
¿Qué credenciales deben considerarse expuestas?
Si una versión afectada se ejecutó, todas las credenciales accesibles para ese proceso deben considerarse potencialmente expuestas:
GitHub
npm
SSH
cloud
CI/CD
deployment
Eso no significa que todas se hayan robado en todos los hosts.
Por qué una dev dependency puede ser crítica
“Solo es codegen” no reduce el riesgo.
El malware solo necesita ejecutarse durante:
npm install
pnpm install
yarn install
Los entornos de desarrollo y CI suelen contener secretos especialmente valiosos.
Instalar dependencias es una superficie de ejecución
Package managers soportan:
preinstall
install
postinstall
prepare
y también flujos como node-gyp.
La instalación es, en la práctica:
resolución
+
descarga
+
posible ejecución de código
¿Fue TeamPCP con certeza?
JFrog, Socket y otros investigadores conectan el código con Mini Shai-Hulud y variantes relacionadas.[3][4]
Pero la atribución exacta del operador no está demostrada de forma definitiva.
Puede existir reutilización de código, acceso residual o copycats.[4]
¿Por qué vuelve Mini Shai-Hulud?
La familia se centra en robo de credenciales, CI/CD, GitHub, registries y propagación.[12]
Patrón:
comprometer release
→ obtener publish authority
→ publicar paquete aparentemente confiable
→ ejecutarse en developer/CI
→ robar nuevas credenciales
El ataque de TanStack en mayo fue distinto
El 11 de mayo fue comprometido el repositorio Router/Start.[11]
La cadena combinó:
pull_request_target,- cache poisoning,
- extracción de token OIDC de memoria del runner.
Resultado:
42 paquetes
84 versiones maliciosas
Query también estuvo limpio en mayo
TanStack indicó explícitamente que Query no fue una familia comprometida.[11]
Mayo:
Router/Start afectado
Agosto:
codegen externo para Query afectado
El incidente de mayo tuvo impacto downstream real
OpenAI confirmó que dos dispositivos de empleados resultaron afectados por el ataque de mayo.[13]
La empresa describió actividad centrada en credenciales y exfiltración limitada de material de credenciales, sin encontrar evidencia de acceso a datos de usuarios ni compromiso de sistemas de producción o propiedad intelectual.[13]
Agosto vs mayo
Mayo
fork PR
→ pull_request_target
→ cache poisoning
→ release confiable
→ extracción OIDC
→ publish
Agosto
fork PR
→ comentario público "npm publish"
→ workflow privilegiado
→ pnpm install
→ id-token: write
→ Trusted Publishing
La ruta de agosto era más simple.
¿Cómo comprobar exposición?
Buscar las 10 versiones en:[1]
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
SBOM
logs CI
dependency caches
No basta con mirar el package.json actual.
IOCs importantes
Indicadores públicos:[1][2][5]
3FWCvzduYZg.js
binding.gyp
is_it_this_simple.js
nu.js
/tmp/trinnyyyy-*/bun
/tmp/*/updater.py
También revisar descargas inesperadas de Bun, gh auth token, llamadas a Git credential manager y node-gyp donde no debería existir native code.
Remediación
Si una versión afectada se ejecutó:[1][2]
1. detener builds
2. aislar host
3. usar máquina limpia
4. rotar credenciales accesibles
5. invalidar sesiones
6. revisar audit logs
7. purgar caches
8. descartar artifacts del runner
9. reconstruir desde un entorno limpio
Versiones seguras
GitHub Advisory señala 3.0.2 como known-good y dice que versiones publicadas antes del 28 de agosto no están afectadas por este incidente.[1]
StepSecurity indica como predecesoras limpias:
0.5.3
1.6.2
2.2.0
3.0.2
El 31 de agosto npm vuelve a mostrar latest = 3.0.2.[6]
¿Qué corrigió upstream?
Según el advisory:[1]
- eliminó el trigger
issue_comment, - movió releases a tag push,
- revocó el Trusted Publisher npm,
- rotó tokens de larga duración,
- restauró
latesta3.0.2, - eliminó el dist-tag
pre, - deprecó las 10 versiones,
- solicitó su retirada en npm.
Snyk también clasifica el compromiso como Critical, con CVSS 9.6, reforzando la gravedad de la ejecución de código malicioso durante la instalación.[8] También son relevantes las medidas adicionales de hardening aplicadas por TanStack tras el incidente de mayo para caches, workflows y release pipelines.[14]
Cómo endurecer GitHub Actions
- No ejecutar código de forks en jobs privilegiados.
- Añadir authorization gates a automatizaciones por comentario.
- Limitar
id-token: writeal publish job real. - Separar build y publish.
- Usar trusted trigger o approval.
- Fijar Actions de terceros a commit SHA.[9]
Cómo endurecer la instalación de dependencias
Controles útiles:
- lockfiles,
- frozen installs,
- minimum release age/cooldown,
- monitorización de releases,
- detección de anomalías de tamaño,
- política de lifecycle scripts,
- CI sandboxed,
- restricciones outbound,
- detección de malware además de CVE,
- SBOM,
- credenciales mínimas en runners.
Trusted Publishing sigue siendo útil.[10]
Pero debe combinarse con Trusted Workflow Design.
Checklist de producción
Dependencies
- Lockfile obligatorio.
- CI bloquea cambios inesperados.
- Release cooldown/minimum age.
- Nuevas versiones no entran directamente en producción.
- Monitor de cambios de tamaño.
- Revisión de install scripts.
-
binding.gypinesperado genera alerta. - Malware scanning además de CVE.
GitHub Actions
- Código de fork nunca se ejecuta con privilegios.
-
issue_commenttiene authorization gate. -
pull_request_targetsolo cuando es necesario. -
GITHUB_TOKENmínimo. -
id-token: writesolo donde hace falta. - Publish requiere trusted trigger.
- Build y publish separados.
- Actions fijadas por SHA.
- La automatización por comentarios tiene un test negativo para usuarios no autorizados.
- El release workflow nunca hace checkout de código de forks no confiables.
npm
- Trusted Publisher limitado.
- Sin long-lived publish tokens innecesarios.
- Monitorización de publicaciones.
- Alertas por releases inesperados.
- Provenance verificada, nunca usada como malware verdict.
- Runbook para deprecation/revocation.
Runners
- CI runners efímeros.
- Secrets mínimos.
- Cloud credentials de corta duración.
- SSH keys con scope mínimo.
- Outbound monitorizado.
- Dependency install sin acceso global.
- EDR/runtime monitoring.
- Aislamiento rápido de host.
Incident response
- Owner definido.
- Búsqueda histórica en lockfiles.
- Purga rápida de CI caches.
- Invalidación de sesiones.
- Mapa de credenciales accesibles.
- Rotación desde máquina limpia.
- Revisión de npm publishes.
- Revisión de GitHub audit logs.
Veredicto POLPROG
El compromiso de @7nohe/openapi-react-query-codegen demuestra cómo está cambiando el software supply-chain security.
El atacante no necesitó hackear npm, robar una contraseña de maintainer, romper 2FA ni obtener un token clásico.
Bastó con introducir código no confiable en un workflow que ya tenía autoridad legítima de publicación.
La frontera se desplaza hacia:
CI/CD
workflow triggers
OIDC
release authorization
dependency execution
Segunda lección: provenance.
No falló criptográficamente. Certificó correctamente el origen de un build malicioso.
Tercera lección: precisión.
TanStack Query no fue comprometido.
Fue comprometida una herramienta de terceros de su ecosistema.
Al 31 de agosto, las versiones maliciosas están identificadas, el advisory está publicado, latest volvió a 3.0.2 y el workflow de release fue modificado.[1][6]
La pregunta clave para cualquier equipo JavaScript no es únicamente:
¿Usamos este paquete?
Sino:
¿Puede un input no confiable llegar en nuestro CI/CD a un job con autoridad para publicar?
Si no hay una respuesta inmediata, conviene auditar GitHub Actions.

