Clonar un repositorio y ejecutar npm install parece un primer paso rutinario para comprobar si funciona. En un proyecto desconocido también es una decisión de seguridad: autorizamos a un gestor de paquetes a resolver un gran conjunto de dependencias y, según la versión y la configuración, ejecutar código con los permisos de nuestro usuario.
Esos permisos pueden incluir acceso al directorio personal, claves SSH, tokens almacenados en variables de entorno, credenciales de servicios cloud, repositorios privados y conexión a Internet.
El código tampoco necesita esperar a que iniciemos la aplicación. Los scripts definidos por el proyecto y, cuando la política lo permite, los scripts de sus dependencias pueden ejecutarse durante la instalación.

Los ataques recientes al ecosistema npm no significan que el open source sea intrínsecamente inseguro. Sí demuestran algo más concreto: confiar en el nombre de un paquete, su popularidad o la reputación histórica de su mantenedor no garantiza que una versión recién publicada contenga exactamente el código que esperamos.
Dos campañas, dos lecciones distintas
En septiembre de 2025, un atacante obtuvo mediante phishing las credenciales de un mantenedor y publicó versiones modificadas de paquetes muy populares, entre ellos chalk y debug.
Los paquetes afectados acumulaban conjuntamente más de dos mil millones de descargas semanales. Las versiones maliciosas estuvieron disponibles durante aproximadamente dos horas y contenían código destinado a manipular direcciones de criptomonedas en las aplicaciones que las cargaban.
La ventana fue corta, pero el alcance potencial era enorme.
La primera lección es temporal: una automatización que adopta una nueva versión pocos minutos después de publicarse puede actuar antes de que la comunidad detecte el problema y esa versión sea retirada.
Pocos días después, Shai-Hulud mostró otro riesgo. El malware combinaba cuentas comprometidas, scripts de instalación, robo de secretos y mecanismos de autorreplicación para intentar extender el ataque a otros paquetes. npm terminó retirando cientos de paquetes asociados.
La segunda lección es de propagación: una máquina de desarrollo o un runner de CI que dispone de credenciales de publicación puede convertir un consumidor comprometido en un nuevo punto desde el que continuar el ataque.
Ambos incidentes afectan a la cadena de suministro, pero no son el mismo ataque ni requieren exactamente las mismas defensas.
No todos los ataques comprometen a un mantenedor
El typosquatting utiliza un nombre visualmente parecido al de un paquete legítimo y espera que alguien lo instale por error.
Por ejemplo:
paquete-popular
paquete-popualr
El atacante no necesita controlar el paquete original. Le basta con aprovechar un error humano, una documentación incorrecta o una automatización mal configurada.
La confusión de dependencias (dependency confusion) explota otra situación.
Una organización puede utilizar internamente un nombre de paquete que termina siendo conocido fuera de la empresa. Un atacante registra entonces ese mismo nombre en un registro público. Si clientes, proxies o registros virtuales mezclan fuentes públicas y privadas sin una configuración adecuada, podría seleccionarse el paquete público en lugar del interno.
En algunas configuraciones, una versión SemVer superior puede favorecer al candidato externo. No es un comportamiento universal de npm y publicar simplemente una versión 99.0.0 no garantiza el ataque. Depende de cómo estén configurados los registros y la resolución de dependencias.
Para paquetes internos es recomendable utilizar scopes y asociarlos explícitamente al registro correspondiente:
@miempresa:registry=https://registro.interno.example/
El proxy o registro debe acompañar esta configuración evitando que esos nombres terminen resolviéndose silenciosamente contra fuentes públicas.

Scopes, registros permitidos, lockfiles y controles de red resuelven problemas diferentes. Ninguno sustituye por sí solo a los demás.
Qué ejecuta realmente npm
package.json permite definir scripts arbitrarios asociados a distintos momentos del ciclo de vida.
En el proyecto raíz, comandos como npm install o npm ci pueden ejecutar eventos como preinstall, install, postinstall o prepare, dependiendo de la configuración y de la versión utilizada.
Los scripts tampoco tienen que estar escritos en JavaScript. npm los entrega al shell de la plataforma, por lo que pueden ejecutar programas, acceder a archivos, iniciar procesos o realizar conexiones de red con los permisos disponibles.
Las dependencias añaden otra superficie de ejecución. Históricamente, sus scripts de instalación podían ejecutarse automáticamente salvo que se bloquearan mediante opciones como --ignore-scripts.
npm 12 introduce una política más restrictiva para los scripts de las dependencias, que deben ser autorizados explícitamente para ejecutarse. Las dependencias procedentes de Git o recursos remotos también reciben restricciones adicionales.
Por tanto, debemos distinguir dos superficies:
- El proyecto que hemos clonado, cuyos propios scripts pueden ejecutar código.
- Sus dependencias, que pueden contener código ejecutable durante instalación, build o runtime.
Bloquear scripts durante la instalación reduce una oportunidad importante de ejecución. No convierte el código instalado en confiable.
Un lockfile resuelve versiones, no intención
Un lockfile conserva una representación concreta del árbol de dependencias junto con información como versiones, ubicaciones resueltas y valores de integridad.
npm ci exige un lockfile existente, verifica su coherencia con package.json, elimina el node_modules previo e instala el árbol definido sin modificar el manifiesto ni el propio lockfile.
Esto mejora enormemente la reproducibilidad, pero no responde a preguntas como:
- ¿La versión ya era maliciosa cuando se creó el lockfile?
- ¿El cambio del lockfile fue revisado?
- ¿El artefacto publicado corresponde al código fuente esperado?
- ¿La cuenta que publicó la versión seguía bajo control de su propietario?
- ¿El paquete ejecutará después un comportamiento malicioso en runtime?
Un lockfile comprometido puede ser simplemente una receta reproducible para instalar siempre el mismo artefacto comprometido.
Su función principal es reproducibilidad e integridad, no demostrar legitimidad.
npm audit tampoco es un certificado de seguridad
npm audit analiza las dependencias utilizando información sobre vulnerabilidades conocidas y puede proponer remediaciones.
Es útil para problemas que ya han sido identificados y catalogados. No garantiza la detección de una versión maliciosa recién publicada, una vulnerabilidad todavía desconocida o una lógica insegura específica de nuestra aplicación.
Un resultado como:
0 vulnerabilities
no significa que el proyecto sea seguro. Significa que el análisis no ha encontrado vulnerabilidades conocidas aplicables al árbol evaluado bajo esas condiciones.
npm audit signatures responde a otra pregunta.
Las firmas permiten comprobar la integridad del artefacto publicado y las attestations de procedencia pueden establecer una relación verificable entre un paquete, su código fuente y determinado proceso de construcción.
Eso tampoco demuestra que el código sea benigno.
Un proceso autorizado puede construir código vulnerable o malicioso y un mantenedor legítimo puede aprobar accidentalmente un cambio inseguro.
La procedencia nos ayuda principalmente a responder a la pregunta: ¿de dónde ha salido este artefacto?

Esta distinción es importante:
- Lockfile: ayuda a reproducir exactamente las dependencias.
npm audit: detecta problemas conocidos.- Firmas: ayudan a comprobar integridad y autenticidad del artefacto.
- Procedencia: aporta información verificable sobre su origen y construcción.
- Sandbox: limita lo que puede hacer código no confiable.
Ninguno demuestra por sí solo que una dependencia sea segura.
Primera apertura de un repositorio desconocido
Antes de ejecutar el gestor de paquetes merece la pena realizar algunas comprobaciones.
1. Leer package.json
Conviene revisar especialmente:
scripts;packageManageryengines;- dependencias desconocidas;
- dependencias Git, URL o fichero;
overrides;- workspaces;
- configuración específica del gestor.
Un script aparentemente sencillo puede limitarse a ejecutar otro fichero. Hay que seguir esa referencia hasta entender qué terminará ejecutándose.
2. Revisar la configuración
.npmrc puede modificar registros, proxies, certificados y políticas de instalación.
También deben revisarse las configuraciones equivalentes de pnpm o Yarn y cualquier script utilizado para preparar el entorno.
En proyectos con paquetes privados es especialmente importante comprobar la relación entre scopes y registros y evitar que nombres internos puedan resolverse accidentalmente desde fuentes públicas.
3. Revisar el lockfile
No es razonable leer manualmente miles de entradas en cada pull request, pero sí automatizar un análisis que destaque:
- paquetes añadidos o eliminados;
- cambios de origen;
- saltos inesperados de versión;
- nuevas dependencias con scripts;
- cambios masivos no relacionados con la tarea.
El lockfile debe tratarse como parte del cambio de código, no como un fichero generado que se acepta automáticamente.
4. Comprobar el artefacto cuando sea necesario
El contenido visible en un repositorio y el tarball publicado en npm no tienen por qué ser exactamente iguales.
Para dependencias especialmente sensibles puede tener sentido inspeccionar el artefacto publicado y contrastarlo con la información de procedencia disponible.
El README, las estrellas o el número de descargas aportan contexto sobre el proyecto. No demuestran qué contiene una versión concreta.
La primera instalación debería hacerse sin secretos
Cuando el repositorio es desconocido, una estrategia prudente consiste en asumir que podría ejecutar código y reducir las consecuencias si realmente ocurre.
El entorno inicial puede utilizar:
- contenedor o máquina virtual descartable;
- usuario sin privilegios;
- directorio personal sin claves SSH;
- ausencia de tokens y credenciales innecesarias;
- variables de entorno mínimas;
- directorios sensibles montados sólo en lectura;
- red bloqueada o con salida controlada;
- ningún acceso al socket Docker del host;
- workspace separado del resto de repositorios.
Un contenedor que tiene montado el socket Docker del host no constituye una frontera de seguridad fuerte. Tampoco uno que ejecuta como root y tiene acceso de escritura a carpetas sensibles de la máquina.
Cuando necesitamos impedir la ejecución de los scripts definidos en package.json, puede utilizarse:
npm ci --ignore-scripts
Después pueden habilitarse únicamente las construcciones que realmente sean necesarias.
Algunos paquetes nativos y determinadas herramientas necesitan legítimamente ejecutar scripts. El objetivo no es bloquearlos siempre, sino convertir su ejecución en una decisión explícita.
npm 12 cambia el modelo por defecto
npm 12 introduce una mejora importante: los scripts de instalación de las dependencias dejan de considerarse automáticamente autorizados.
Los paquetes que necesitan ejecutarlos deben incorporarse a la política de autorización del proyecto.
El cambio conceptual es relevante.
Antes, el modelo habitual era aproximadamente:
Instalar
↓
Ejecutar scripts salvo que los bloqueemos
El modelo de npm 12 se acerca más a:
Instalar
↓
Script necesario
↓
¿Está autorizado?
↓
Sí → ejecutar
No → bloquear
Las dependencias Git y otros orígenes remotos reciben también controles más restrictivos.
Pero esto no elimina el problema.
Una dependencia autorizada puede contener una vulnerabilidad o comportarse de forma maliciosa cuando la aplicación la importe o la ejecute. Bloquear un postinstall elimina una vía de ejecución temprana, no el riesgo asociado al código que posteriormente formará parte del programa.
El tiempo también puede reducir riesgo
Las versiones maliciosas que generan mucha actividad pueden ser detectadas y retiradas en cuestión de horas.
Adoptar automáticamente cada release en el momento exacto de su publicación aumenta la exposición durante esa primera ventana.
Dependabot aplica actualmente un periodo de espera por defecto para determinadas actualizaciones normales de versión, mientras que las actualizaciones de seguridad siguen un tratamiento distinto.
La idea importante no es un número concreto de días, sino diferenciar situaciones:
- corrección de seguridad urgente;
- nueva versión funcional recién publicada;
- dependencia interna con un pipeline controlado;
- paquete crítico que requiere verificaciones adicionales.
Retrasar una actualización funcional puede dar tiempo a que aparezcan señales procedentes de usuarios, mantenedores, herramientas de seguridad o el propio registro.
No sustituye a otros controles y tampoco detectará un ataque que permanezca oculto durante mucho tiempo.
Una política práctica como consumidor
No existe una medida única que proteja la cadena de suministro.
Una estrategia razonable combina varias capas:
- Mantener sólo dependencias que tengan una justificación.
- Congelar y revisar el lockfile.
- Controlar los registros y fuentes permitidos.
- Separar claramente paquetes públicos y privados.
- Bloquear scripts de dependencias salvo necesidad.
- Revisar las excepciones.
- Evitar adoptar automáticamente releases recién publicadas cuando no sea necesario.
- Utilizar análisis de vulnerabilidades y secretos.
- Verificar firmas y procedencia cuando estén disponibles.
- Construir en entornos aislados y con los mínimos secretos posibles.
- Mantener un inventario de dependencias que permita reaccionar rápidamente ante un incidente.
Herramientas como overrides de npm, resolutions de Yarn o los mecanismos equivalentes de pnpm pueden ayudar a sustituir temporalmente una dependencia vulnerable que se encuentre dentro del grafo.
Deben utilizarse con cuidado. Forzar otra versión puede introducir incompatibilidades y siempre debe acompañarse de actualización del lockfile, pruebas y documentación del motivo.
También existen herramientas como OpenSSF Scorecard que aportan señales sobre las prácticas de mantenimiento de un proyecto. Son información adicional, no una garantía automática de seguridad.
El mantenedor controla la otra mitad del problema
Quien publica paquetes debe proteger el recorrido entre código fuente, construcción y registro.
Entre las medidas más importantes están:
- autenticación resistente al phishing;
- credenciales con el mínimo alcance posible;
- trusted publishing mediante OIDC;
- workflows protegidos frente a código no confiable;
- separación entre entornos de pruebas y publicación;
- aprobación adicional antes de publicar;
- información de procedencia;
- revisión periódica de permisos;
- procedimientos preparados para revocar credenciales.
Un runner utilizado para probar una pull request no debería disponer automáticamente de una credencial capaz de publicar una nueva versión.
Si el código comprometido consigue acceder a la credencial más potente del entorno, el propio diseño de permisos le habrá proporcionado el siguiente paso del ataque.
Si sospechamos una instalación comprometida
Borrar node_modules y volver a instalar no es suficiente.
Si código malicioso pudo ejecutarse, cualquier credencial accesible para ese proceso podría haber quedado expuesta.
Una respuesta razonable consiste en:
- Aislar el equipo o runner afectado.
- Identificar paquete, versión, periodo y comandos ejecutados.
- Consultar indicadores y avisos oficiales.
- Revocar o rotar las credenciales potencialmente expuestas.
- Revisar publicaciones, workflows y accesos posteriores.
- Reconstruir el entorno desde una fuente verificada y una máquina limpia.
- Comunicar claramente qué está confirmado y qué constituye únicamente una posible exposición.
Las nuevas credenciales deben generarse desde un entorno considerado limpio. Cambiarlas desde la misma máquina comprometida puede exponer también las sustitutas.
Conclusión
npm install no es peligroso porque npm sea excepcionalmente inseguro.
Es una operación sensible porque incorpora un gran número de componentes externos a un sistema que posteriormente ejecutaremos con nuestros permisos, secretos y acceso a red.
La solución tampoco consiste en dejar de utilizar dependencias ni en revisar manualmente todo el ecosistema.
Consiste en introducir controles en los momentos donde aportan más valor: antes de ejecutar código desconocido, antes de adoptar una release recién publicada, antes de entregar secretos y antes de conceder capacidad de publicación.
Un lockfile, npm audit, una firma, la procedencia, una política de scripts y un sandbox responden a preguntas diferentes.
La seguridad mejora cuando entendemos qué demuestra cada mecanismo y, especialmente, qué sigue sin estar demostrado.



