- Se produjo un ataque a la cadena de suministro en el ecosistema de NPM en el que se inyectó malware autorreplicante en más de 40 paquetes, incluido el popular @ctrl/tinycolor, lo que puede desencadenar una infección en cadena de secretos del entorno de desarrollo e incluso credenciales de CI/CD. Las versiones comprometidas ya fueron eliminadas de npm
- La carga útil del ataque ejecuta de forma asíncrona durante el proceso de instalación de npm un bundle de Webpack (
bundle.js, ~3.6MB) y realiza una recolección amplia de credenciales mediante variables de entorno, el sistema de archivos y SDKs de nube - La lógica maliciosa usa NpmModule.updatePackage para forzar el parcheo y despliegue de otros paquetes, provocando propagación en cascada, e inyecta un workflow shai-hulud en GitHub Actions para robar secretos de la organización mediante
toJSON(secrets) - Los datos recolectados se exfiltran mediante la creación del repositorio público de GitHub
Shai-Hulud, disfrazándose como actividad de desarrollo legítima y con alta capacidad de evasión de detección - Opera de forma encubierta mediante acceso a tokens y endpoints de metadatos de AWS/GCP/Azure/NPM/GitHub, además de búsqueda de secretos basada en TruffleHog
- Se exige la eliminación inmediata de paquetes, limpieza de repositorios y rotación total de credenciales, junto con revisión de logs de CloudTrail/GCP Audit, bloqueo de webhooks e implementación de políticas de protección de ramas/Secret Scanning/enfriamiento
Affected Packages
- Se reportaron 195 paquetes/versiones en total; entre los principales están @ctrl/tinycolor(4.1.1, 4.1.2), múltiples paquetes del namespace @ctrl/, módulos de @crowdstrike/, además de ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-color en todo el ecosistema de Angular/UI web, el stack móvil de @nativescript-community/ y @nstudio/, el toolchain de ciencias de la vida de teselagen/, ember-*, koa2-swagger-ui, pm2-gelf-json, wdio-web-reporter y otros
- Para la versión exacta de cada paquete, se debe consultar la tabla del texto original y verificar con precisión si esas versiones están en uso
- Ejemplos:
@ctrl/ngx-emoji-mart 9.2.1, 9.2.2,@ctrl/qbittorrent 9.7.1, 9.7.2,ngx-bootstrap 18.1.4, 19.0.3–20.0.5,ng2-file-upload 7.0.2–9.0.1y otros de amplio alcance
- Ejemplos:
Immediate Actions Required
Identify and Remove Compromised Packages
- Verificar si existen paquetes comprometidos en el proyecto: revisar con
npm ls @ctrl/tinycolor, etc. - Eliminar de inmediato los paquetes comprometidos: ejecutar
npm uninstall @ctrl/tinycolor, etc. - Buscar hashes conocidos de
bundle.jspara revisar rastros locales: usarsha256sum | grep 46faab8a...
Clean Infected Repositories
- Eliminar el workflow malicioso de GitHub Actions: quitar
.github/workflows/shai-hulud-workflow.yml - Detectar y borrar la rama remota
shai-huludcreada en el repositorio: después degit ls-remote ... | grep shai-hulud, ejecutargit push origin --delete shai-hulud
Rotate All Credentials Immediately
- Es necesaria la rotación total de tokens de NPM, secretos de GitHub PAT/Actions, claves SSH, credenciales de AWS/GCP/Azure, cadenas de conexión a bases de datos, tokens de terceros y secretos de CI/CD
- También se requiere la rotación completa de los elementos almacenados en AWS Secrets Manager/GCP Secret Manager
Audit Cloud Infrastructure for Compromise
- AWS: revisar en CloudTrail el momento y patrón de llamadas a
BatchGetSecretValue,ListSecretsyGetSecretValue, y usar el IAM Credential Report para detectar creación o uso anómalo de claves - GCP: revisar en Audit Logs los accesos a Secret Manager y confirmar si existen eventos
CreateServiceAccountKey
1 comentarios
Opiniones de Hacker News
Desde la perspectiva de alguien que usa paquetes alojados en npm, siento que no es realista vigilar manualmente todas las dependencias y las dependencias de esas dependencias; además, no soy experto en TypeScript/JavaScript, así que probablemente no detectaría fácilmente malware oculto por un atacante. Últimamente he estado pensando en una forma de actualizar en “modo diferido”, es decir, actualizar no a la versión más reciente sino solo a versiones que ya tengan cierto tiempo. La idea es que, si un paquete ha estado expuesto al público unas 6 semanas, es más probable que ya se hubiera descubierto cualquier malware. No es un método perfecto, pero me gustaría que existiera una herramienta con una opción para aplicar excepcionalmente la actualización más reciente de inmediato cuando haya un problema de seguridad
Justamente el artículo menciona un método así: existe una función llamada NPM Package Cooldown Check. Si se agrega a un pull request una versión de paquete publicada dentro del período configurado por la organización (2 días por defecto), el build falla automáticamente. Como la mayoría de los ataques a la cadena de suministro se detectan dentro de 24 horas, incluso una espera muy corta puede reducir la exposición de seguridad
Como es difícil inspeccionar todas las dependencias, quisiera defender la idea de reducir al máximo la cantidad de dependencias y usar solo paquetes conocidos y confiables. De hecho, a menos que estés en un entorno tan controlado como para confiar en todos los autores, mantener cierto grado de
not-invented-herees una decisión sensataTengo la costumbre de fijar explícitamente las versiones en
package.jsony usarnpm cipara instalar solo las versiones especificadas enpackage-lock.json. En CI ejecutonpm auditpara recibir alertas cuando aparece una vulnerabilidad en un paquete. Así los paquetes quedan casi “congelados”, y solo por la antigüedad del paquete ya disminuye la probabilidad de infecciónEn mi caso voy aún más lejos y solo actualizo dependencias cuando un bug realmente afecta mi entorno de uso. Incluso si hay una vulnerabilidad de seguridad, si no me impacta, la dejo pasar. La mayoría de los desarrolladores actualiza dependencias con demasiada frecuencia sin necesidad, pero creo que lo correcto es hacerlo solo cuando de verdad hace falta. Si un paquete requiere actualizaciones frecuentes o es complejo, prefiero no usarlo o lo “congelo” según mi criterio
En Python también se puede limitar actualizaciones de forma similar usando uv. Por ejemplo, con un comando como
uv lock --exclude-newer $(date --iso -d \"2 days ago\")puedes excluir versiones publicadas dentro de los últimos 2 díasEste problema ocurre porque los paquetes o versiones nuevos no están suficientemente vigilados. Lo mejor sería operar por separado una distribución estable al estilo Debian, donde solo se aplican parches de seguridad y correcciones de bugs, y distribuciones testing/unstable vigiladas por mantenedores de paquetes. Todos los que trabajan con repositorios centrales de paquetes abiertos (NPM, Python, Rust, etc.) enfrentan el mismo problema
Hay un problema cultural entre desarrolladores. Tener cientos de dependencias (transitivas) y actualizarlas automáticamente sin pensarlo es parte del problema. Exponer el entorno de build/ejecución a tantísimo código de terceros conlleva responsabilidad
Las distribuciones también sienten cada vez más la carga de mantener tantos paquetes. De hecho, por eso crecieron los ecosistemas por lenguaje (por ejemplo, CPAN, Maven, RubyGems, etc.). Solo con distribuciones Linux era difícil ofrecer las apps que querían los usuarios, así que surgieron vías como freshmeat, linuxbrew, flatpak y PPA. No creo que todas las comunidades tengan la capacidad de vigilar y dar soporte a múltiples ramas de una enorme variedad de librerías
Como desarrollador de Debian, cada vez me cuesta más detectar cambios reales antes de incorporar código upstream por culpa de tanto “ruido”, especialmente cambios de estilo o actualizaciones de tooling. Ojalá se evitaran esos cambios, salvo cuando sean refactorizaciones, correcciones de bugs, nuevas funciones o resultados de herramientas destinados realmente a encontrar código problemático y que sí requieran revisión humana
En Rust existe un sistema llamado cargo vet. Empresas como Google y Mozilla participan compartiendo y verificando paquetes automáticamente
Creo que hay formas de mantener cierta descentralización y aun así añadir medidas de seguridad. Por ejemplo, exigir aprobación de dos cuentas con 2FA para paquetes por encima de cierto tamaño, o permitir que los paquetes populares se suban a npm solo mediante sistemas de builds reproducibles. No haría falta renunciar por completo a la distribución descentralizada; apenas implicaría un poco más de trabajo en proyectos grandes
Debido a la seguidilla reciente de ataques a la cadena de suministro, estoy considerando más seriamente el server rendering (sin JavaScript). Gracias a HTMX me di cuenta de lo lejos que se puede llegar sin JavaScript, y además parece que así las apps serían más rápidas y estables
Quiero subrayar que el entorno tradicional de JS es en realidad el sandbox más seguro. Durante casi 30 años se ha ejecutado código JS no confiable en miles de millones de dispositivos, y los casos de ataques masivos exitosos contra motores de navegador se cuentan con los dedos. Pero el entorno de NodeJS y npm necesita una reestructuración total en términos de seguridad. Casos como leftpad vienen de la cultura de subir hasta simples fragmentos de código a npm
Me parece raro que estos ataques se reduzcan automáticamente a un problema de un entorno específico (JavaScript). En realidad, el problema mayor es que ni siquiera las medidas de endurecimiento que ya existen en npm se aplican en otros entornos como PyPI o Crates
El vendoring puede reducir la exposición, pero no me parece una solución de fondo. Si NPM tomara la seguridad en serio, debería exigir 2FA y escaneo previo del paquete al publicar, e incluso forzar firmas con llaves de hardware. Semver o CRC no bastan. Todo esto debería venir integrado por defecto en el sistema de gestión de paquetes
En realidad, este tipo de ataques no es exclusivo de JavaScript; surge porque los desarrolladores no vigilan lo suficiente cuando agregan nuevas dependencias. Eso aplica igual a otros ecosistemas como Rust o Go
Todos los lenguajes que dependen mucho de gestores de paquetes y tienen bibliotecas estándar pobres son igual de vulnerables. A largo plazo, creo que deberíamos volver más hacia JavaScript vanilla. Rust también tiene una alta dependencia de paquetes. De hecho, Go es un caso ejemplar en este tema
Creo que hace falta un sistema capaz de rastrear código ligero que permita firmar commits/releases con llaves confiables, e instalar y verificar todo eso. Ya existe el enfoque de provenance de npm con sigstore, pero por ahora no parece estar muy extendido y más bien se limita a la verificación del emisor
Ya en 2016 se había reportado esta vulnerabilidad a NPM (aviso de CERT), pero la respuesta de NPM fue WAI (working as intended)
Para quien no sepa qué significa WAI: por lo general es la sigla de “working as intended”
Incluso si no hubiera scripts de postinstall, al importar el módulo durante el build, el arranque del servidor o las pruebas, el malware igual terminaría ejecutándose. Al final, siempre hay algún momento después de
npm installen el que algo realmente se ejecuta...Me acordé de un comentario que vi aquí durante el caso de left-pad: hablaba de un mantenedor famoso de npm con 600 paquetes npm y 1,200 líneas de código JavaScript. Un caso que pondría como ejemplo es esbuild, que casi no tiene dependencias externas y usa solo la biblioteca estándar de Go.
Incluso otros proyectos llamados “de próxima generación”, si ves su cadena de dependencias, como biomejs y swc, también tienen relativamente pocas. Pero si miras el código fuente original en Rust, tanto biomejs como swc igual tienen muchas dependencias. Si este tipo de proyectos se expande, supongo que el ecosistema de cargo seguirá el mismo camino. Si alguien conoce proyectos grandes escritos con una disciplina tan estricta como esbuild, agradecería recomendaciones
Una de las razones por las que me pasé a Go fue la tendencia de librerías estilo purego. Normalmente dependen solo de la biblioteca estándar y de golang.org/x, y pueden compilarse sin CGO, así que son muy portables.
go mod vendorsirve para gestionar el riesgo a corto plazo, pero no es una solución de fondo. Go tampoco ofrece verificación de paquetes de extremo a extremo (firmas/verificación de llaves, etc.), así que la vulnerabilidad sigue ahí. En particular, mucho se concentra en la infraestructura de CI/CD, pero si se pudiera construir y desplegar sin trasladar llaves de firma, la seguridad también podría mejorar. Creo que los gestores de paquetes deberían fomentar firmas GPG y distribuirse con firmas también en los commits de gitEl caso de eslint me resulta especialmente frustrante. Si ves su grafo de dependencias, es enorme, y si el mantenedor no prioriza reducir dependencias, al final no queda otra que cambiarse a otra solución como oxlint
La respuesta es implementar uno mismo las funciones fáciles y reducir dependencias externas. Normalmente, con solo hacer eso ya puedes recortar dos tercios de las dependencias totales. En especial cosas simples como left-pad conviene hacerlas uno mismo y mantenerlas bajo control con unidades pequeñas y tests; la carga de mantenimiento no es tan grande. Hay que eliminar sin miedo las dependencias innecesarias
Lo que aparece en el
Cargo.tomlraíz de un proyecto Rust es para todo el workspace, y las dependencias reales de cada crate son mucho más superficiales. Hay que mirar con más profundidad para entender la estructura real de dependenciasLa desventaja es que ahora, para inspeccionar un proyecto JavaScript, también hay que leer Golang. Y encima en post-install luego vuelve a ejecutarse
node install.js, así que al final no queda otra que confiar totalmente o leer todo el códigoNo puedo creer que npm todavía ejecute por defecto los scripts
postinstallde todas las dependencias. Pnpm o Bun solo los ejecutan si están en una lista de permitidos, y Composer ni siquiera ejecuta scripts de ciclo de vida para dependencias. Me parece que ese enfoque es más seguro por el riesgo que representan los paquetes dependientes en entornos de build o desarrolloMe pregunto por qué no se oyen ataques masivos así con tanta frecuencia en otros gestores de paquetes (por ejemplo, Rust
build.rs, Python, Java, etc.). Más allá depostinstall, en principio es posible en casi todos los ecosistemas, pero los incidentes parecen concentrarse sobre todo en npmVi que el valor por defecto de Pnpm cambió para bloquear scripts. Tengo curiosidad por la reacción de la comunidad (la experiencia de uso al permitir scripts, el posible abuso del comando allow, etc.), y en la comunidad de empaquetado de Python también se están dando discusiones parecidas en torno a las variantes de wheel. Me gustaría tomar como referencia la experiencia de otros ecosistemas
Este ataque ya se expandió a más de 180 paquetes; ver el blog de Aikido Security
Me da curiosidad quién fue la primera persona en detectar este ataque. Es interesante ver cómo distintos blogs se atribuyen el mérito de maneras diferentes. Aikido dice “descubrimos un ataque a gran escala”, y Socket, Ox, Safety, Phoenix, Semgrep, etc. también lo describen cada uno a su manera
Soy Mackenzie de Aikido. La primera persona en reportar el caso fue el desarrollador Daniel Pereira, quien se lo pasó a Socket, y Socket fue quien primero analizó los 40 paquetes iniciales y el malware. Después, Aikido encontró 147 paquetes adicionales e incluso el paquete de Crowdstrike. De hecho, Step fue quien primero se dio cuenta de que el malware era un gusano autoreplicante. Es interesante que varias organizaciones hayan cumplido papeles distintos de forma independiente
Parece que varios desarrolladores lo detectaron casi al mismo tiempo, y Step y Socket mencionan a personas distintas. En última instancia, distintos proveedores de seguridad de la industria lo detectaron cada uno a su manera: análisis de código con IA (Socket, Aikido) o monitoreo de pipelines con eBPF (Step)
Si tantos proveedores lo detectaron de forma independiente, me pregunto si no podrían compartir esa tecnología directamente con npm para bloquear desde el principio el registro de paquetes maliciosos. Supongo que si lo hicieran ya no podrían vender un sistema de alertas tempranas
El artículo de OP cita textualmente: “@franky47 detectó este fenómeno y enseguida alertó a la comunidad con un issue de GitHub”
Me parece bastante ingenioso el nombre que eligió el atacante, “Shai Hulud”: le puso a un gusano informático el nombre de un gusano gigante. Incluso el
bundle.jsprincipal pesa 3.6 MB, así que hasta el malware terminó creciendo muchísimo al estilo npmTengo el presentimiento de que pronto algún ataque a la cadena de suministro terminará activando accidentalmente otro ataque a la cadena de suministro
El malware también sigue la ley de Moore: en 1991 el tequila virus pesaba 2.6 KB, y ahora ya son varios MB