Detección de Shai-Hulud sobre NPM
El resurgimiento del ataque «Shai-Hulud» a npm
¿Qué ha ocurrido?
El 4 de agosto de 2026, el ecosistema JavaScript ha sufrido una nueva oleada del ataque conocido como Shai-Hulud.
A través del secuestro de la cuenta de GitHub de un mantenedor, los atacantes inyectaron código malicioso en librerías populares de almacenamiento en caché como keyv y la familia @cacheable. En cuestión de cuatro horas, el ataque se ha propagado de forma automatizada a más de 400 paquetes (alcanzando más de 800 en recuentos posteriores) con un impacto acumulado de miles de millones de descargas.
¿Cómo funciona la infección?
- Inyección en el origen (Primera vía):El atacante usó la cuenta comprometida de GitHub para introducir código malicioso directamente en el repositorio de la librería. Al construirse a través de los flujos de trabajo legítimos (como GitHub Actions), el paquete resultante incluía firmas y certificados de procedencia válidos, lo que permitió esquivar las herramientas de seguridad convencionales.
- Propagación automatizada (Segunda vía):Una vez que un desarrollador o servidor infectado instalaba el paquete, la carga útil buscaba tokens de autenticación de npm almacenados en
~/.npmrc. Si el token tenía permisos de escritura y la opciónbypass_2faactiva, el malware descargaba automáticamente los paquetes que ese usuario gestionaba, les inyectaba el código malicioso, incrementaba la versión y los volvía a publicar sin intervención humana ni dejar rastro de commits o pull requests. - Mecanismo de ejecución (
Dropper):- Se incluye un script ligero en el hook
"preinstall": "node setup.mjs". setup.mjsdescarga un entorno de ejecución de Bun (para evadir el monitoreo tradicional de Node.js) y ejecuta la carga útil principal (math_init.js/Math_Symbol.js), cuyo objetivo es robar credenciales.
- Se incluye un script ligero en el hook
¿Por qué está teniendo tanto alcance?
Librerías como keyv o flat-cache rara vez son instaladas directamente por los usuarios; llegan como dependencias transitivas de herramientas masivas como ESLint o clientes HTTP.
Esto significa que proyectos que no habían modificado su código terminan expuestos en múltiples capas de profundidad dentro de sus dependencias.
¿Cómo protegerse y mitigar el riesgo?
- La mejor defensa (npm 12+): En npm 12 y versiones posteriores, los scripts del ciclo de vida (
preinstall) están desactivados por defecto.
Si se utilizanpm install --ignore-scripts, el código malicioso no se ejecuta aunque el archivo infectado llegue a la carpetanode_modules. - Seguridad en tokens: Evitar tokens con permisos de omisión de 2FA (
bypass_2fa) y forzar autenticación de dos factores en cada publicación (auth-and-writes). - Auditoría de dependencias: Revisar detalladamente los archivos de bloqueo (
package-lock.json) para verificar si se resolvieron versiones específicas contaminadas (publicadas a partir del 4 de agosto de 2026).
Detección de Shai-Hulud
He creado un script en Python para la detección en tus repositorios de código.
Además te incluyo el protocolo de respuesta ante una infección confirmada.
1. Explicación del Script de Python (shai_hulud_detector.py)
El script está diseñado para realizar una auditoría estática y dinámica en tres niveles sobre cualquier proyecto o repositorio Node.js dentro del directorio de trabajo actual:
Nivel 1: Detección de Droppers Físicos (Análisis Estático)
- ¿Qué busca?: Archivos con nombres clave que utiliza el malware (
setup.mjs,math_init.js,Math_Symbol.js). - ¿Cómo lo hace?: Escanea recursivamente el árbol de archivos. Si encuentra un paquete susceptible en
node_modules(comoflat-cacheofile-entry-cache), comprueba si dentro de su carpeta existen dichos archivos dropper. Si la dependencia está presente pero no contiene los droppers, la marca como LIMPIA [OK].
Nivel 2: Inspección de Configuración (package.json)
- Hooks de ciclo de vida (
preinstall): Analiza si el campo"scripts"de algúnpackage.jsonintenta ejecutar comandos sospechosos (por ejemplo,"preinstall": "node setup.mjs"). - Versiones infectadas conocidas: Contrasta las versiones declaradas e instaladas con la lista negra de publicaciones maliciosas confirmadas (
keyv@6.0.0,cacheable@2.5.1,cache-manager@7.2.10, etc.).
Nivel 3: Auditoría del Árbol de Dependencias (Análisis Dinámico)
- Equivalente a
npm list: Identifica las raíces de los proyectos Node.js (donde hay unpackage-lock.jsononode_modules) y ejecuta automáticamente una inspección del árbol de dependencias resueltas para las familias de riesgo (keyv,cacheable,flat-cache,file-entry-cache, etc.).
Esto permite visibilizar la cadena de dependencias transitivas (por ejemplo, ver si llegan a través deeslint).
2. Procedimiento de Actuación en Caso de Infección Confirmada
Si el script o tus revisiones confirman la presencia de un dropper o de una versión infectada, se debe asumir que el entorno ha sido comprometido y aplicar inmediatamente el siguiente protocolo:
Paso 1: Aislamiento del Entorno
- Detener procesos activos: Cancela inmediatamente cualquier servidor, pipeline de CI/CD o proceso de construcción (
npm run build,npm start) que se esté ejecutando en ese equipo. - Desconectar de la red: Si es una máquina o servidor en producción, aíslala temporalmente de la red para evitar que la carga útil descargue el entorno Bun o intente exfiltrar credenciales.
Paso 2: Revocación Inmediata de Credenciales (Crítico)
Como el objetivo principal de Shai-Hulud es el robo y reutilización de credenciales:
- Revocar Tokens de NPM: Entra en tu cuenta de npmjs.com y revoca todos los Access Tokens activos inmediatamente.
- Revocar Tokens de GitHub / CI/CD: Revoca cualquier Personal Access Token (PAT), clave SSH o credenciales de integración de CI/CD (GitHub Actions, Gitea, etc.) que estuvieran almacenadas en la máquina infectada (
~/.npmrc,~/.ssh, variables de entorno).
Paso 3: Limpieza y Eliminación
Limpiar la caché global de npm:
npm cache clean --force
Borrar directorios y bloqueos contaminados: Elimina las carpetas node_modules y el archivo package-lock.json del proyecto afectado:
rm -rf node_modules package-lock.json
Paso 4: Reinstalación Segura
- Fijar versiones seguras: Asegúrate en tu
package.jsonde que las dependencias estén apuntando a versiones anteriores estables o a las versiones parcheadas que hayan publicado los mantenedores oficiales.
Desactivar scripts automáticos: Fuerza la instalación sin ejecutar los hooks de ciclo de vida para evitar que cualquier dependencia infectada vuelva a dispararse:
npm install --ignore-scripts
Paso 5: Auditar Publicaciones (Si eres mantenedor)
Si tenías permisos de publicación en algún paquete de npm en esa máquina:
- Revisa tu perfil en el registro de npm para comprobar si el gusano publicó versiones no autorizadas (versiones parche como
x.x.+1) a tu nombre el 4 de agosto o en fechas posteriores. - Despublica (unpublish) o marca como deprecadas (deprecate) de inmediato las versiones maliciosas generadas en tus paquetes.
Script de Detección
Puedes realizar un cortar-pegar del código para la detección, puedes crear el fichero con el siguiente nombre shai_hulud_detector.py (en esta segunda versión he ampliado la revision del análisis):
#!/usr/bin/env python3
import os
import json
import subprocess
# Indicadores de Compromiso (IoCs): Archivos dropper usados por el gusano
DROPPER_FILES = ["setup.mjs", "math_init.js", "Math_Symbol.js"]
# Paquetes a auditar y sus versiones estrictamente maliciosas confirmadas
KNOWN_VULNERABLE_PACKAGES = {
"keyv": ["6.0.0"],
"cacheable": ["2.5.1"],
"@cacheable/memory": ["2.2.1"],
"@cacheable/net": ["2.1.1"],
"@cacheable/node-cache": ["3.1.2"],
"@cacheable/utils": ["2.5.1"],
"cache-manager": ["7.2.10"],
"cacheable-request": ["13.0.20"]
}
TARGET_PACKAGES = [
"keyv", "cacheable", "cacheable-request", "cache-manager",
"flat-cache", "file-entry-cache"
]
def check_dropper_files(package_dir):
"""Inspecciona el directorio de un paquete en busca de archivos dropper."""
found_droppers = []
for dropper in DROPPER_FILES:
path = os.path.join(package_dir, dropper)
if os.path.exists(path):
found_droppers.append(path)
return found_droppers
def run_npm_list_audit(project_dir):
"""Ejecuta una inspección de dependencias equivalente a 'npm list'."""
print(f"\n[+] Ejecutando auditoría del árbol de dependencias en: {project_dir}")
pkgs_str = " ".join(TARGET_PACKAGES)
try:
result = subprocess.run(
f"npm list {pkgs_str}",
shell=True,
cwd=project_dir,
capture_output=True,
text=True
)
output = result.stdout.strip() or result.stderr.strip()
print("--- Árbol de dependencias resuelto ---")
print(output)
print("--------------------------------------")
except Exception as e:
print(f"[!] No se pudo ejecutar npm list: {e}")
def scan_directory(root_dir):
print(f"[*] Iniciando escaneo avanzado contra 'Shai-Hulud' desde: {os.path.abspath(root_dir)}\n")
warnings_found = 0
projects_with_npm = set()
for dirpath, dirnames, filenames in os.walk(root_dir):
if ".git" in dirnames:
dirnames.remove(".git")
# 1. Comprobación directa de archivos dropper en la ruta actual
for file in filenames:
if file in DROPPER_FILES:
file_path = os.path.join(dirpath, file)
print(f"[CRÍTICO] Archivo dropper malicioso detectado directamente en: {file_path}")
warnings_found += 1
# 2. Análisis de package.json
if "package.json" in filenames:
pkg_path = os.path.join(dirpath, "package.json")
# Registrar si es la raíz de un proyecto de Node.js
if "node_modules" in dirnames or os.path.exists(os.path.join(dirpath, "package-lock.json")):
projects_with_npm.add(dirpath)
try:
with open(pkg_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Verificar scripts preinstall sospechosos
scripts = data.get("scripts", {})
preinstall = scripts.get("preinstall", "")
if any(dropper in preinstall for dropper in DROPPER_FILES):
print(f"[CRÍTICO] Hook preinstall infectado en: {pkg_path}")
print(f" Comando: \"preinstall\": \"{preinstall}\"")
warnings_found += 1
# Inspección de instalación local si este package.json pertenece a un nodo instalado
pkg_name = data.get("name", "")
if pkg_name in TARGET_PACKAGES or any(pkg_name.startswith(p) for p in ["@cacheable/"]):
# Verificar si este paquete en node_modules contiene un dropper físicamente
droppers = check_dropper_files(dirpath)
if droppers:
print(f"[CRÍTICO] ¡ALERTA CONFIRMADA! Dropper hallado dentro del paquete '{pkg_name}':")
for d in droppers:
print(f" -> {d}")
warnings_found += 1
else:
version = data.get("version", "desconocida")
# Verificar versión contra la lista de infecciones confirmadas
if pkg_name in KNOWN_VULNERABLE_PACKAGES and version in KNOWN_VULNERABLE_PACKAGES[pkg_name]:
print(f"[ALERTA] Versión infectada conocida hallada: {pkg_name}@{version} en {dirpath}")
warnings_found += 1
else:
print(f"[OK] Paquete de riesgo presente pero LIMPIO (Sin droppers, versión segura): {pkg_name}@{version}")
except (json.JSONDecodeError, PermissionError):
continue
# 3. Ejecutar la comprobación automática del árbol en los proyectos encontrados
for proj in projects_with_npm:
run_npm_list_audit(proj)
print("\n" + "="*60)
if warnings_found == 0:
print("[OK] Escaneo finalizado: No se detectaron archivos dropper ni versiones maliciosas.")
else:
print(f"[RESULTADO] Se encontraron {warnings_found} amenazas reales/críticas. Revisa los detalles arriba.")
print("="*60)
if __name__ == "__main__":
scan_directory(os.getcwd())Si no quieres teclear simplemente puedes descargarte el fichero y darle permisos de ejecucion, el script revisa todos los subdirectorios desde la ruta en que se lanza, con lo que puedes revisar de golpe varios repositorios.
También podeis descargarlo desde mi repositorio GIT: