Blog / Actualidad de seguridad

Compromiso de axios en npm: cómo se rompió la cadena de confianza

El compromiso de axios en npm no fue un fallo de código. Siga los seis eslabones de la cadena de confianza, el control que rompe cada uno y lo que cambia npm 12.

Estudios de doctorado en Seguridad de la Información completadosDefensa universitaria superada · Defensa final de la tesis pendiente

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 npm

La 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.

Ocho 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 credentials

Fuentes

¿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
Todos los artículos

08Contacto

¿Va a lanzar algo que trata datos de usuarios?

Envíeme un breve resumen. Respondo en un plazo de tres días con preguntas o un esbozo de propuesta, y el precio se acuerda por escrito antes de empezar.