El código estaba bien. El editor, no
Dos registros del repositorio, una versión de diferencia. Axios 1.14.0 lo publicó GitHub Actions a través de un editor de confianza y lleva procedencia (provenance). Axios 1.14.1, que apareció a las 00:21 UTC del 31 de marzo de 2026, se publicó a mano, sin nada de eso. Esa diferencia es toda la historia del compromiso de axios en npm, y estaba ahí, en los metadatos del repositorio. [2]
Los hechos, en pocas palabras. Axios 1.14.1 y 0.30.4, publicados con 39 minutos de diferencia, añadieron una nueva dependencia, plain-crypto-js@4.2.1, cuyo script postinstall dejaba caer un troyano de acceso remoto en cualquier máquina que ejecutara la instalación. npm retiró ambas versiones antes de las 03:15 UTC. [1][2] Google atribuye la operación con alta confianza a UNC1069, un grupo vinculado a Corea del Norte, y Microsoft rastrea al actor como Sapphire Sleet. [3][4]
A esto lo llamo un ataque a la cadena de confianza. Un ataque a la cadena de suministro de software cambia el código que usted ejecuta sin cambiar una sola línea que haya revisado, comprometiendo el canal que entrega una dependencia. Nadie explotó un fallo en el código fuente de axios. Cada eslabón fue una decisión de confianza que alguien tomó, y esa es la forma útil de leer el incidente. No he reproducido nada de esto por mi cuenta; el análisis se apoya en el informe posterior de los mantenedores, en informes de proveedores y en los registros del repositorio tal como estaban el 7 de octubre de 2026.
axios@1.14.0 published by CI
_npmUser GitHub Actions
trustedPublisher github
gitHead 46bee3dea75ef53a8eae49f3b7487e6341de6074
dist.attestations SLSA provenance v1
axios@1.14.1 2026-03-31T00:21:58Z, published by hand
_npmUser the lead maintainer's account
trustedPublisher absent
gitHead absent
GitHub commit/tag none; the release exists only on npmLa cadena empezó en un portátil y terminó en un portátil
El primer eslabón no fue npm ni GitHub. Según el informe posterior de los mantenedores, el atacante llegó al equipo del mantenedor principal mediante una campaña dirigida de ingeniería social y malware de tipo RAT, lo que le dio las credenciales de la cuenta npm usadas para publicar. [1] Un RAT en la máquina de un desarrollador produjo el token. El token produjo un RAT en cada máquina de desarrollador y cada runner de CI que instaló la versión envenenada. La misma clase de malware en ambos extremos.
Los mantenedores fueron directos al respecto. Publicar directamente desde una cuenta personal era un riesgo evitable, y el flujo OIDC y las versiones inmutables que están adoptando ahora deberían haber estado vigentes antes de que esto ocurriera. [1] Respeto esa frase. Nombra la lección real, y esa lección no trata de axios, sino de dónde reside la autoridad para publicar.
Leo el incidente como seis eslabones, mostrados a continuación. En cada uno, un control podría haber roto la cadena, y cada uno tiene un responsable distinto. Algunos pertenecen al editor. La mayoría le pertenecen a usted. Los recorro en orden.
1 maintainer laptop social engineering + RAT -> npm credentials
2 registry gate direct token publish accepted -> axios 1.14.1, 0.30.4
3 decoy dependency plain-crypto-js 4.2.0 -> 4.2.1 -> new line in package.json
4 your resolver fresh install, no cooldown -> poisoned tarball fetched
5 install script postinstall: node setup.js -> dropper runs
6 runner or laptop reachable secrets, open egress -> RAT, then whatever it can reach¿Por qué el trusted publishing de OIDC no detuvo un token robado?
Porque no era la única puerta. El trusted publishing permite que el repositorio acepte una publicación solo desde un flujo de CI determinado, usando una credencial OIDC de vida corta en lugar de un secreto almacenado. Las versiones de axios 1.x ya pasaban por ese camino, por eso 1.14.0 muestra un editor de confianza y procedencia. [2][6] La versión maliciosa 1.14.1 se publicó directamente desde una cuenta, sin editor de confianza, sin gitHead y sin ningún commit o etiqueta correspondiente en el repositorio. [2]
Un buen camino de publicación no elimina el malo. Mientras el paquete siga aceptando tokens, quien tenga uno puede saltarse por completo el flujo de trabajo. El control que cierra esa vía es un ajuste del paquete, Require two-factor authentication and disallow tokens, que la documentación de npm recomienda una vez configurados los editores de confianza, y que deja a esos editores de confianza funcionando con normalidad. [6] StepSecurity interpreta los metadatos ausentes como un token clásico de larga duración robado; los mantenedores solo dicen que el atacante obtuvo las credenciales de la cuenta. [1][2] En cualquier caso, la publicación nunca pasó por el flujo de trabajo.
Mi opinión: activar OIDC sin desactivar los tokens es medio control, y muchos equipos marcan la casilla tras completar solo la primera mitad. Si publica paquetes, revise ambas mitades hoy mismo. Si los consume, los metadatos del repositorio son una señal que puede leer. Una versión que antes venía de un editor de confianza y de repente deja de venir así merece una pausa. npm también está apretando el otro lado: desde enero de 2027, los tokens granulares que eluden la autenticación de dos factores perderán la capacidad de publicar directamente. [7]
Una dependencia que nada importa es, en sí misma, un hallazgo
El atacante preparó el terreno un día antes. plain-crypto-js@4.2.0 se publicó a las 05:57 UTC del 30 de marzo como un señuelo limpio: una copia del código legítimo de crypto-js sin ningún hook de instalación. Su única función era darle al paquete un historial de publicaciones para que no pareciera recién creado. La versión maliciosa 4.2.1 llegó a las 23:59 UTC, 22 minutos antes que axios 1.14.1. [2]
Y ahora, la pista. StepSecurity examinó los 86 archivos de axios@1.14.1 y descubrió que plain-crypto-js no se importa ni se requiere en ningún lugar. [2] El código que axios distribuía no tenía ningún uso para ella. La versión también eliminó el script prepare de husky, lo que encaja con una publicación manual que se saltó las herramientas de release habituales. [2] Dicho sin rodeos: una versión de parche añadió una dependencia de tiempo de ejecución que el paquete nunca usa. Es una línea ante la que un revisor humano se detiene.
Es la comprobación que automatizaría en primer lugar. En cada actualización, compare el manifiesto entre la versión antigua y la nueva. Una dependencia nueva en un salto de parche va a parar a una persona. Luego, tres preguntas: ¿cuán antiguo es el paquete, quién lo mantiene y lo usa realmente el paquete padre? Los comandos siguientes responden a las tres en menos de un minuto. Su bot de actualizaciones no se las hace por usted.
# what changed in the manifest between two versions
npm diff --diff=<pkg>@<old> --diff=<pkg>@<new> package.json
# does the package actually use the new dependency?
npm pack <pkg>@<new> && tar -xzf <pkg>-<new>.tgz && grep -R "<new-dep>" package/
# how old is the new dependency, and who maintains it?
npm view <new-dep> time maintainers --json¿Por qué bastó un npm install para entregar la máquina?
Porque npm ejecuta código durante la instalación. Un script de ciclo de vida es un comando que un paquete declara en su package.json y que el gestor de paquetes ejecuta automáticamente; plain-crypto-js@4.2.1 declaraba postinstall: node setup.js. [3] Eso se ejecuta antes de que su aplicación importe nada, por lo que un proyecto que nunca tocó la biblioteca en tiempo de ejecución quedó igualmente expuesto. StepSecurity observó la primera llamada al servidor del atacante a los dos segundos de un npm install. [2]
La ofuscación de setup.js es más endeble de lo que parece. Las cadenas se invierten, se decodifican de Base64 y se someten a XOR con la clave OrDeR_7077 y la constante 333. [5] La desofuscación de StepSecurity muestra que la clave pasa por Number(): sus letras se convierten en NaN, el XOR con cero no cambia nada, y la clave efectiva es 0,0,0,0,0,0,7,0,7,7. [2] Es un truco contra escáneres de cadenas de texto, no criptografía. La lección: el análisis estático de scripts de instalación es un buen sistema de alarma, pero un límite débil.
Después, el dropper se ramifica según la plataforma y pide al servidor del atacante una segunda etapa. El cuerpo del POST indica al servidor cuál enviar: product0 para macOS, product1 para Windows, product2 para Linux. [2] La tabla muestra dónde aterriza cada una. Las tres variantes emiten una señal cada 60 segundos, un JSON codificado en Base64, con el User-Agent de Internet Explorer 8 en Windows XP. [3] Como el RAT se descarga en vivo, nada en el tarball es, propiamente, el malware: el paquete es un descargador. Eso convierte el tráfico saliente en el control que importa, y un User-Agent de IE8 procedente de un runner de compilación en la alerta más barata que jamás escribirá. [3]
platform stage two lands in started by
win32 %TEMP%\6202033.ps1 VBScript; powershell.exe copied to %PROGRAMDATA%\wt.exe
darwin /Library/Caches/com.apple.act.mond Mach-O binary, chmod +x, zsh in the background
linux /tmp/ld.py nohup python3
all POST http://sfrclak[.]com:8000/6202033
body: packages.npm.org/product0 (macOS), product1 (Windows), product2 (Linux)
beacon: Base64 JSON every 60 seconds
User-Agent: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
win32 persistence: HKCU Run key "MicrosoftUpdate" and %PROGRAMDATA%\system.bat¿Por qué la carpeta parecía limpia después?
Porque el dropper se encargó de ordenar. Tras ejecutarse, setup.js se autoeliminaba junto con el package.json malicioso, y después renombraba un package.md preparado de antemano como package.json. [2] Ese manifiesto de sustitución declara la versión 4.2.0 sin ningún hook de instalación. Un ingeniero que abra después node_modules/plain-crypto-js ve una inofensiva 4.2.0, aunque la que se ejecutó fue la 4.2.1. [2] El señuelo sembrado un día antes rinde sus frutos por segunda vez.
Por eso les digo a los equipos que revisen el lockfile, no node_modules. Una entrada de plain-crypto-js en el lockfile, ya sea 4.2.0 o 4.2.1, significa que su grafo de instalación incluyó el ataque, y la recomendación de Google es dar por comprometido el host a partir de ese momento. [3] Qué conservar y cómo demostrar la recuperación se trata en mi artículo anterior de Axios sobre evidencias de compilación. Aquí me centro en el mecanismo.
¿Qué hacía el RAT? El análisis de Google denomina la carga útil WAVESHAPER.V2; incluye reconocimiento del sistema, listado de directorios, ejecución arbitraria de comandos e inyección de binarios. [3] Eso es acceso, no robo. Lo que un operador hace con acceso a un portátil de desarrollador o a un runner de CI es leer los tokens, las claves y los archivos .env que hay ahí; el caso de OpenAI muestra lo que está en juego: un flujo de trabajo de GitHub Actions en su proceso de firma de aplicaciones de macOS ejecutó axios 1.14.1 con un certificado de firma y material de notarización al alcance. OpenAI no encontró evidencias de que se accediera a datos de usuario ni de que su software se alterara, y rotó el certificado como precaución. [12]
¿Quién debe preocuparse realmente por esto?
Una parte más pequeña de su infraestructura de la que sugerían los titulares, y más concreta. La aplicación distribuida no es la víctima. Nada importa plain-crypto-js, así que un bundle de frontend construido a partir de axios 1.14.1 no tenía motivo para contener la carga útil; la víctima es la máquina que ejecutó la instalación. [2] La ventana también fue corta: de 00:21 UTC a la retirada a las 03:15 UTC, menos de tres horas. [1]
Para que esto le afectara, tenían que cumplirse tres condiciones. Que una instalación nueva resolviera axios 1.14.1 o 0.30.4 en esa ventana, es decir, una instalación sin un lockfile coincidente o un bot de actualizaciones que fusionara en cuestión de horas. Que los scripts de instalación tuvieran permiso para ejecutarse, que era el comportamiento por defecto. Y que la máquina tuviera algo que mereciera la pena alcanzar. Durante el incidente, la recomendación de Microsoft fue fijar versiones exactas y pausar los bots automáticos de dependencias. [4] Si puede responder a esas tres preguntas para cada runner y cada portátil, conoce su exposición. Si no puede, ese es el verdadero hallazgo.
¿Qué control rompe qué eslabón?
Uno por eslabón, y la tabla siguiente es todo el modelo. Romper un solo eslabón basta para detener un ataque concreto. Usted no elige qué eslabón dejará intacto el próximo atacante, así que cubra más de uno.
El eslabón cinco cambió este año. npm 12, disponible de forma general desde el 8 de julio de 2026, ya no ejecuta los scripts preinstall, install y postinstall de las dependencias a menos que el proyecto los permita, registrados en una lista allowScripts que usted confirma en el repositorio. [7][8] En npm 12, el hook de plain-crypto-js no se habría ejecutado sin un permiso explícito. Pero compruebe qué usa realmente su CI: la imagen node:24-alpine incluye npm 11.19.0 a fecha del 7 de octubre de 2026, y npm 11 todavía ejecuta los scripts de las dependencias por defecto. pnpm los bloquea por defecto desde la versión 10, e incluso ese ajuste necesitaba verificación: antes de la 10.26.0, las dependencias alojadas en Git aún podían ejecutar código durante la instalación, un fallo de gravedad alta. [9][10] Un ajuste se convierte en un límite real una vez que lo ha visto bloquear algo.
El eslabón cuatro también tiene un control económico. npm 11.10.0 añadió min-release-age, un periodo de espera en días, en febrero de 2026. El minimumReleaseAge de pnpm se mide en minutos y en pnpm 11 vale un día por defecto. [10][11] Las versiones envenenadas de axios vivieron menos de tres horas, así que incluso un día de espera las supera con creces. Un periodo de espera es una apuesta a que alguien note algo dentro de esa ventana, y no sirve de nada contra un atacante paciente. Combínelo con trustPolicy: no-downgrade de pnpm, que rechaza una versión cuyo nivel de confianza haya bajado respecto a versiones anteriores. [10] Está pensado exactamente para un caso como el de 1.14.1, pero no he repetido este ataque contra él, porque 1.14.1 ya no está en el repositorio. Pruébelo en un paquete que usted controle.
link control owner
1 maintainer laptop no publish credential on a workstation publisher
2 registry gate trusted publisher + "disallow tokens" publisher
3 decoy dependency diff the manifest; question new deps in patches you
4 fresh resolution npm ci + lockfile; min-release-age / pnpm cooldown you
5 install script npm 12 allowScripts; pnpm 10+; --ignore-scripts you
6 reachable secrets short-lived creds, split jobs, egress allow-list you
+ trust downgrade pnpm trustPolicy: no-downgrade youOcho comprobaciones antes de su próximo npm install
Aquí tiene la versión de este artículo sobre la que puede actuar. Empiece por sus propios repositorios, después sus imágenes de CI y, por último, los paquetes que publica. La lista siguiente está ordenada según la rapidez con la que da resultado.
Cuando reviso una base de código, el manifiesto de dependencias, el lockfile y la configuración de release entran en el alcance, porque los límites de confianza que importan suelen estar ahí y no en el código que usted escribió. Esa es la parte de la cadena a la que un pentest normal no suele llegar. Si quiere la mirada de un atacante sobre cómo su producto confía en sus dependencias y en su camino de release, cuénteme qué está lanzando. Si cree que se vio afectado, conserve las evidencias antes de reconstruir y recurra a un especialista en respuesta a incidentes: ese es un trabajo distinto del mío.
1 lockfile grep -nE "plain-crypto-js" package-lock.json # 4.2.0 or 4.2.1 means act
2 CI install npm ci, never npm install; no bot merges within hours of a release
3 scripts npm 12: review allowScripts | older npm: --ignore-scripts, allow by name | pnpm 10+: allowBuilds
4 cooldown npm config set min-release-age 3 | pnpm: minimumReleaseAge
5 trust pnpm: trustPolicy: no-downgrade
6 publishing your packages: trusted publisher on, "disallow tokens" on, no publish token on a laptop
7 egress build runners: default-deny outbound; alert on node spawning curl, powershell or python3
8 secrets the install job holds no deploy, signing or cloud credentialsFuentes
- [1] Mantenedores de Axios: informe posterior al incidente
- [2] StepSecurity: axios comprometido en npm, análisis técnico
- [3] Google Threat Intelligence: actor vinculado a Corea del Norte ataca axios
- [4] Microsoft: mitigación del compromiso de axios en npm
- [5] JFrog Security Research: análisis del malware plain-crypto-js
- [6] Documentación de npm: editores de confianza
- [7] Registro de cambios de GitHub: seguridad de npm en la instalación y fin de los tokens que eluden 2FA
- [8] Documentación de npm: scripts de instalación en npm 12
- [9] Aviso de seguridad de pnpm GHSA-379q-355j-w6rj: elusión del bloqueo de scripts de ciclo de vida
- [10] Documentación de pnpm: ajustes de seguridad de la cadena de suministro
- [11] Versión v11.10.0 de npm CLI: min-release-age
- [12] OpenAI: compromiso de la herramienta de desarrollo axios
¿Necesita que se compruebe esto en su propio producto?
Revisión de seguridad del código¿Quiere aprender a encontrar vulnerabilidades como esta por su cuenta?
Curso individual de revisión de código seguro