Saltar al contenido
Volver al blog
Desarrollo

npm v12 dejó de ejecutar los scripts de instalación: qué cambia en tu pipeline

Wilson Vargas Martínez 10 min21 de septiembre de 2026

Tras un año de gusanos que se propagan solos por el registro, npm apagó por defecto los scripts de instalación, las dependencias de Git y las URL remotas. Ninguna versión de Node.js trae ese npm todavía: hay que pedirlo.

Desde el 8 de julio de 2026, un npm install con npm v12 no ejecuta los scripts de instalación de tus dependencias, no resuelve dependencias de Git y no baja tarballs desde URL remotas, salvo que lo autorices de forma explícita. La versión 12.0.0 de la CLI salió ese día y la 12.0.2 el 27 de julio. Es el cambio de comportamiento por defecto más agresivo en la historia del registro, y llega después de doce meses en los que un gusano que se propaga solo comprometió primero 500 paquetes y después más de 1.300 versiones.

El detalle que descoloca a los equipos: ninguna línea de Node.js empaqueta todavía ese npm. Actualizar Node no te entrega los defaults nuevos. Hay que pedirlos, y hay que preparar el repositorio antes, porque el primer efecto visible es un build que falla.

¿Qué pasó en npm entre septiembre de 2025 y agosto de 2026?

Dos oleadas del mismo gusano, con un año de distancia y un salto de escala. En la primera, CISA emitió una alerta el 23 de septiembre de 2025 por más de 500 paquetes comprometidos por un malware bautizado Shai-Hulud: escaneaba el entorno buscando tokens personales de GitHub y claves de API de AWS, GCP y Azure, las exfiltraba a un repositorio público mediante la API de GitHub y después se autenticaba en el registro como el desarrollador comprometido para publicar versiones infectadas de otros paquetes suyos. Ese último paso es lo que lo convierte en gusano.

La segunda oleada es de agosto de 2026. El aviso AD-2026-009 de la agencia de ciberseguridad de Singapur, del 6 de agosto, cifra en más de 1.300 las versiones de paquetes comprometidas por la variante ChainDrop, con unos 2.000 millones de descargas mensuales combinadas. La lista de afectados explica el alcance: keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10. Nadie instala esos paquetes a propósito; llegan tres o cuatro niveles abajo, debajo de ESLint o de un cliente HTTP.

¿Cómo entran, si nadie instaló un paquete raro?

Por el token de publicación de un mantenedor legítimo, y 2026 dejó tres formas distintas de conseguirlo, todas documentadas por las víctimas.

  • Desde la memoria del runner de CI. El 11 de mayo, un atacante publicó 84 versiones maliciosas en 42 paquetes de TanStack. El método, según el postmortem: localizó el proceso Runner.Worker de GitHub Actions y leyó /proc/<pid>/maps y /proc/<pid>/mem para extraer el token OIDC que el ejecutor mantenía en memoria porque el workflow declaraba id-token: write. Un investigador externo lo detectó entre 20 y 26 minutos después; las versiones quedaron marcadas como obsoletas en una hora y 43 minutos.
  • Desde el editor del desarrollador. El compromiso que Red Hat documenta en RHSB-2026-006 empezó el 29 de mayo con una cuenta de GitHub tomada mediante una extensión de VS Code con malware, y terminó con 32 paquetes publicados bajo el espacio de nombres @redhat-cloud-services. Ningún producto de Red Hat se construyó con esas versiones, pero el vector importa: el editor del portátil es parte de la cadena de suministro.
  • Sin comprometer a nadie, por confusión de nombres. El 28 de mayo, un actor publicó 14 paquetes maliciosos en una ventana de cuatro horas, con metadatos de repositorio falseados y números de versión inflados —1.0.7265— para simular un historial maduro. La segunda etapa era un binario de unos 195 KB compilado con Bun que buscaba credenciales de AWS en Secrets Manager de más de 16 regiones, tokens de HashiCorp Vault, tokens de publicación de npm y secretos de GitHub Actions.

Ese tercer caso es el que explica el cambio de npm. El robo ocurría durante la instalación, mediante los hooks de ciclo de vida del paquete. No hacía falta que el código de la víctima llamara a nada: bastaba con que alguien escribiera npm install.

¿Qué cambió exactamente en npm v12?

Tres rutas de ejecución automática pasaron a ser opt-in, según el anuncio del 9 de junio:

  1. Scripts de instalación. preinstall, install y postinstall de las dependencias no corren salvo que el paquete esté aprobado en el campo allowScripts de tu package.json.
  2. Dependencias de Git. --allow-git queda en none: ni directas ni transitivas, lo que cierra una vía de ejecución de código que podía activarse desde un .npmrc.
  3. URL remotas. --allow-remote queda en none: los tarballs HTTPS fuera del registro también hay que autorizarlos.

Hay más cambios de ruptura en la misma versión que conviene mirar antes de actualizar: npm view --json devuelve siempre un array, desaparece el soporte de npm-shrinkwrap.json, y se eliminan npm adduser y los comandos star, stars y unstar. El que rompe pipelines silenciosamente es el de --json, si algún script lee ese resultado con jq.

¿Ya tengo npm v12 si actualicé Node.js?

No. A hoy, el índice de distribuciones de Node.js muestra que la línea 26 empaqueta npm 11.19.1, la línea 24 —el LTS activo, según el calendario de soporte— empaqueta npm 11.19.0 y la 22 sigue en npm 10.9.8. Ninguna línea trae la 12, así que la versión nueva se instala aparte: npm install -g npm@12, o fijando la versión en el gestor que uses.

Esto tiene una consecuencia operativa concreta. Si tu CI usa una imagen node:24 y confías en el npm que trae, estás corriendo los defaults antiguos aunque tu equipo crea lo contrario. La práctica que seguimos es fijar la versión exacta de npm en el pipeline y tratarla como una dependencia más, no heredarla de la imagen base.

¿Cómo migro un repositorio sin romper el build?

Preparándolo con npm 11 antes de tocar npm 12. Todo el comportamiento nuevo está disponible detrás de avisos desde npm 11.16.0, así que la migración se puede ensayar sin cambiar de versión mayor:

  1. npm install con normalidad, para que la CLI registre qué paquetes traen scripts.
  2. npm approve-scripts --allow-scripts-pending, que solo lista los paquetes pendientes sin aprobar nada.
  3. Revisar esa lista, aprobar con npm approve-scripts lo que tenga sentido, negar el resto con npm deny-scripts y confirmar el package.json resultante al repositorio.
  4. Correr el pipeline completo con --strict-allow-scripts, que aplica el comportamiento de v12 estando todavía en npm 11. Si pasa, la actualización de versión mayor deja de ser un salto al vacío.

Existe npm approve-scripts --all para aprobar de golpe todo lo pendiente. Sirve como fotografía inicial en un monorepo con cientos de dependencias, y es mejor eso que posponer la migración seis meses, pero deja el trabajo a medias: lo que aprueba es el estado de hoy, no una decisión. Si se usa, conviene que la lista quede en el package.json y que su crecimiento se revise en cada pull request, que es donde se nota un paquete nuevo pidiendo ejecutar código en la instalación.

¿Qué pasa con los paquetes nativos como sharp o better-sqlite3?

También quedan bloqueados, y por un camino que no se ve en el package.json. Un paquete que incluye binding.gyp dispara un node-gyp rebuild implícito aunque no declare ningún script de instalación, así que npm lo trata como script y lo detiene. sharp, better-sqlite3 y canvas son los casos que aparecen primero en cualquier proyecto real.

El fallo es ruidoso, que es lo bueno: el build revienta en el pipeline, no en producción. Hay dos salidas razonables. Aprobar explícitamente esos paquetes, dejando constancia en el repositorio de qué dependencias tienen permiso para compilar; o preferir las distribuciones con binarios precompilados donde el proyecto las ofrezca, que además acortan el tiempo de construcción de la imagen.

¿Y si el atacante entra por el token de publicación?

Ahí npm cerró en paralelo, y con fechas que ya están corriendo. Los tokens de acceso granular con la opción de saltarse el segundo factor perdieron desde agosto de 2026 la capacidad de hacer operaciones sensibles de cuenta, paquete y organización —crear o borrar tokens, cambiar contraseñas, configurar publicación de confianza, gestionar equipos— y desde enero de 2027 pierden la publicación directa: quedarán en leer paquetes privados y dejar la publicación en espera de una aprobación humana con segundo factor.

El resto de las piezas aparece en el balance que GitHub publicó el 28 de julio: publicación por etapas desde el 22 de mayo, publicación de confianza vía OIDC ampliada a CircleCI desde el 6 de abril, cuentas de alto impacto en modo solo lectura durante 72 horas tras un cambio de correo o el uso de un código de recuperación del segundo factor desde el 25 de junio, y una herramienta de autoservicio para revocar credenciales desde el 24 de junio.

El caso de TanStack matiza el entusiasmo por OIDC: el token era de corta vida y aun así se lo llevaron de la memoria del proceso. La publicación de confianza elimina el secreto de larga duración, que es mucho, pero el trabajo que emite ese token tiene que estar aislado. En los pipelines que montamos, el job que publica no ejecuta nada de terceros, las acciones externas van fijadas por SHA y id-token: write vive solo en ese job, nunca a nivel de workflow.

¿Cómo evito instalar una versión comprometida el día que sale?

Metiendo tiempo entre la publicación y tu instalación. Dependabot aplica desde el 14 de julio un periodo de enfriamiento de tres días por defecto en las actualizaciones de versión, mientras que las actualizaciones de seguridad siguen llegando de inmediato. Esa asimetría es la correcta: la ventana de TanStack duró menos de dos horas, y el enfriamiento la habría cubierto entera sin retrasar un parche de seguridad real.

Lo demás es higiene que ya deberías tener y casi nunca está completa: npm ci en todos los pipelines en vez de npm install, el archivo de bloqueo confirmado al repositorio y revisado en cada pull request, y ninguna etiqueta flotante en producción. Un rango ^ en package.json es inofensivo si el archivo de bloqueo manda; deja de serlo el día que alguien corre npm install en el servidor.

¿Qué hago si ya instalé una versión comprometida?

Tratar la máquina como comprometida y rotar desde otra. Las recomendaciones de CISA y del aviso de Singapur coinciden en el orden: eliminar las versiones afectadas de los entornos de desarrollo y de CI, rotar los tokens personales de GitHub, las claves SSH, las credenciales de nube y los tokens de publicación de npm, revisar los repositorios buscando commits o repositorios públicos nuevos que nadie creó, y bloquear la salida hacia los dominios usados para exfiltrar, con webhook.site a la cabeza.

Dos detalles que se saltan y cuestan caro. Rotar credenciales desde el mismo equipo infectado entrega las nuevas al atacante. Y el escaneo de secretos con protección de ramas activada es lo que convierte el siguiente intento en una alerta en lugar de un incidente; CISA lo pide de forma explícita, junto con un segundo factor resistente a phishing.

Por dónde empezaríamos esta semana

  1. Inventario de scripts. Correr npm approve-scripts --allow-scripts-pending en cada repositorio y contar cuántos paquetes piden ejecutar código en la instalación. El número sorprende.
  2. Fijar la versión de npm en CI, en lugar de heredar la de la imagen base, y ensayar con --strict-allow-scripts antes de saltar a v12.
  3. Migrar la publicación a OIDC en los paquetes internos, con el job de publicación aislado y las acciones de terceros fijadas por SHA.
  4. Activar el enfriamiento de Dependabot y confirmar que las actualizaciones de seguridad siguen sin retraso.
  5. Un ensayo de rotación con cronómetro: cuánto tardan hoy en rotarse todos los tokens de publicación y las credenciales de nube de un equipo. Ese número es tu tiempo real de respuesta.

Cómo te ayudamos en Athrun Data Intelligence

Llamada de 30 minutos para revisar la cadena de suministro de tus repositorios: qué dependencias ejecutan código en la instalación, qué tokens de larga duración siguen vivos y qué tan expuesto está tu job de publicación. Si encaja, dejamos la migración a npm v12 hecha con el inventario de scripts confirmado al repositorio, la publicación por OIDC aislada y un procedimiento de rotación probado con cronómetro, no escrito en un documento.

Fuentes

Quién lo escribió

Siguiente paso

¿Te pasa algo parecido? Pide el diagnóstico.

Nos cuentas el reto y en 24 horas hábiles te respondemos por escrito si es viable y por dónde empezar. Sin compromiso.

Artículos relacionados

  1. 01

    Stack para lanzar un SaaS en 90 días sin acumular deuda técnica

    Cristian Agudelo · 7 min
  2. 02

    Shopify vs headless commerce: cuándo cada uno tiene sentido

    Nodier Solano · 6 min