Vulnerabilidades en NPM: qué son y cómo enfrentarse a ellas

El otro día estaba en un afterwork con otros profesionales del sector y entre una charla y otra nos metimos en un discurso que luego se me quedó en la mente: el peligro sobre las muchas vulnerabilidades que pueden generar las dependencias NPM en nuestros proyectos.

"No es que odie Javascript, es que odio NPM."

NPM es el gestor de paquetes de NodeJS (JavaScript en el backend) y se usa para gestionar dependencias de proyectos en JavaScript (especialmente, pero no solo, en NodeJS). De este modo si necesitamos una funcionalidad ya hecha, la buscamos en NPM y con una simple instrucción la incorporamos al proyecto para poder usarla. A partir de ese momento se puede actualizar automáticamente, y se descarga sola cuando alguien inicia el proyecto de nuevo, aunque no disponga de ella.

Según muchos desarrolladores, sobre todos los más conscientes y aprensivos en temas de seguridad, en los últimos años las vulnerabilidades en el frontend se han hecho mucho más críticas debido al elevado número de paquetes y dependencias que solemos utilizar en nuestros proyectos.

Npm instala cientos o miles de paquetes por cada proyecto, y esto genera árboles de dependencias enormes, prácticamente imposibles de auditar manualmente.
Encima, la mayoría de los paquetes que instalamos en nustros proyectos suelen tener uno o dos desarrolladores que los mantienen únicamente en su tiempo libre.

Cuando NPM tumbó medio Internet: el caso left-pad:

Un conocido caso de 2016 demostró al mundo como 11 líneas de código pueden tumbar medio internet.
Left-pad era un minúsculo paquete NPM cuya función era rellenar una cadena por la izquierda con un carácter hasta alcanzar una longitud específica.


            
  const leftPad = require('left-pad'); 
    console.log(leftPad('abc', 5, '0'));  // Resultado: "00abc"
        

Miles de paquetes dependían directamente e indirectamente de left-pad, incluso grandes frameworks como Babel Javascript Compiler . Por otro lado, NPM no guardaba snapshots de las versiones publicadas, y por lo tanto, cuando alguien eliminaba un paquete esto afectaba los builds de todas sus dependencias en los proyectos.

Un día, el creador de left-pad tuvo una dísputa con NPM, se enfadó y decidió retirar (hacer unpublish) de todos sus paquetes publicados en la plataforma. Incluso el utilizadísimo left-pad.
Esto fue grave, porque en un día la caída de 11 líneas de código afectó a un inimaginable número de aplicaciones.

La respuesta de NPM Inc. fue una decisión nunca antes vista: volvieron a publicar el paquete de manera manual desde sus servidores, restaurando la última versión conocida de left-pad, sin el permiso del autor, para evitar que siguieran fallando proyectos en todo el mundo.
Esto fue muy polémico, porque rompía la filosofía de que los paquetes publicados y eliminados eran decisión exclusiva del autor.

Aunque no sea especifícamente de vulnerabilidad técnica, el caso left-pad ha demostrado al mundo lo frágil que puede llegar a ser el ecosistema NPM. Y aquí os listo unos cuantos casos conocidos de vulnerabilidades en NPM:

  • ☑️ 2018, caso event stream: Un paquete muy popular para manejar streams en Node.js fue comprometido cuando un atacante tomó control del paquete y añadió código malicioso. El código inyectaba un malware para robar bitcoins en proyectos que usaban event-stream. Fue muy preocupante porque el paquete era una dependencia indirecta usada en miles de proyectos.
    ⁉️ Afectaba a producción?
    ❔ Lección: Cuidado con paquetes abandonados o con mantenedores poco activos, ya que pueden ser secuestrados y modificados maliciosamente.
  • ☑️ 2019, caso coa Command Injection: El paquete coa, utilizado por otros paquetes bastante populares, tenía una vulnerabilidad que permitía la ejecución de comandos arbitrarios. Si no se validaban correctamente ciertas entradas, los atacantes podían ejecutar comandos en el sistema del desarrollador.
    ⁉️ Afectaba a producción? Sí,potencialmente.
    Lección: el problema se solucionó rápidamente pero fue una alerta importante sobre el riesgo en dependencias transitivas.
  • ☑️ Caso axios en 2019: La librería HTTP axios tuvo vulnerabilidades donde un atacante podía forzar al servidor a hacer peticiones a direcciones internas (SSRF).Esto puede permitir ataques internos a redes privadas o servicios internos.
    ⁉️ Afectaba a producción?
    Lección: hasta las dependencias más populares y utilizadas pueden ser un peligro para tu proyecto.
  • ☑️ 2018, caso minimist: la librería minimist, usata en muchas herramientas CLI, presentó una vulnerabilidad que permitía la contaminación del prototipo de objetos. Esto permitía la manipulación del código para permitir acceso a maliciosos.
    ⁉️ Afectaba a producción? En muchos casos, sí.
    ❔ Lección: Las vulnerabilidades en paquetes comunes y muy usados, como minimist, pueden propagarse a miles de proyectos sin que los desarrolladores se den cuenta inmediatamente.

Vulnerabilidades en producción vs vulnerabilidades en desarrollo:

Todos los casos que os he presentado hablan de vulnerabilidades que afectaban al entorno de producción de los proyectos, pero no siempre es así. Muchas vulnerabilidades que aparecen en las auditorías están en herramientas que sólo se usan durante el proceso de desarrollo (ejemplo: webpack-dev-server, @angular-devkit, vite…).Dependencias "devDependencies" vs "dependencies": Las vulnerabilidades en los paquetes que están dentro de devDependencies generalmente no se despliegan al servidor de producción, especialmente si haces el build correctamente y despliegas solo el resultado (por ejemplo, el código transpileado y minificado).

Las vulnerabilidades en herramientas como Webpack, Babel o Vite solo tienen riesgo mientras el desarrollador está trabajando en su máquina local, no en el código final que corre en el servidor o en el navegador. Pero esto no las hace menos inofensivas. Vamos a ver un ejemplo concreto de auditoría en npm:

Ejecuta este comando en tu último proyecto npm:

npm audit

A mi, si lo ejecuto en uno de mis proyectos, me sale esto:

npm audit    
# npm audit report

brace-expansion  1.0.0 - 1.1.11 || 2.0.0 - 2.0.1
brace-expansion Regular Expression Denial of Service vulnerability - https://github.com/advisories/GHSA-v6h2-p8h4-qcjw
brace-expansion Regular Expression Denial of Service vulnerability - https://github.com/advisories/GHSA-v6h2-p8h4-qcjw
fix available via `npm audit fix`
node_modules/@npmcli/package-json/node_modules/brace-expansion
node_modules/@tufjs/models/node_modules/brace-expansion
node_modules/brace-expansion
node_modules/cacache/node_modules/brace-expansion
node_modules/ignore-walk/node_modules/brace-expansion
node_modules/node-gyp/node_modules/brace-expansion
node_modules/webpack-dev-server/node_modules/brace-expansion

esbuild  <=0.24.2
Severity: moderate
esbuild enables any website to send any requests to the development server and read the response - https://github.com/advisories/GHSA-67mh-4wv8-2f99
fix available via `npm audit fix --force`
Will install @angular-devkit/build-angular@20.0.4, which is a breaking change
node_modules/esbuild
node_modules/vite/node_modules/esbuild
  @angular-devkit/build-angular  <=19.2.14 || 20.0.0-next.0 - 20.0.0-rc.4
  Depends on vulnerable versions of @angular/build
  Depends on vulnerable versions of esbuild
  Depends on vulnerable versions of webpack-dev-server
  node_modules/@angular-devkit/build-angular
  @angular/build  <=19.2.0
  Depends on vulnerable versions of @vitejs/plugin-basic-ssl
  Depends on vulnerable versions of esbuild
  Depends on vulnerable versions of vite
  node_modules/@angular/build
  vite  0.11.0 - 6.1.6
  Depends on vulnerable versions of esbuild
  node_modules/vite
    @vitejs/plugin-basic-ssl  <=1.1.0
    Depends on vulnerable versions of vite
    node_modules/@vitejs/plugin-basic-ssl

webpack-dev-server  <=5.2.0
Severity: moderate
webpack-dev-server users' source code may be stolen when they access a malicious web site with non-Chromium based browser - https://github.com/advisories/GHSA-9jgg-88mc-972h
webpack-dev-server users' source code may be stolen when they access a malicious web site - https://github.com/advisories/GHSA-4v9v-hfq4-rm2v
fix available via `npm audit fix --force`
Will install @angular-devkit/build-angular@20.0.4, which is a breaking change
node_modules/webpack-dev-server

7 vulnerabilities (1 low, 6 moderate)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Vamos a ver cada una de estas vulnerabilidades qué significa:

  • ☑️ brace-expansion: Regular Expression Denial of Service (ReDoS). Si alguien introduce una expresión regular maliciosa, puede provocar un bloqueo o sobrecarga del proceso.
    • ⁉️ Puede afectar a producción? No, porque sólo se usa en herramientas de desarrollo o en tu proceso de build.
    • ❔ Dónde se encuentra: Lo usan indirectamente herramientas como webpack-dev-server, node-gyp, cacache, etc.
  • ☑️ esbuild: SSRF-like vulnerability. Puede permitir que una web maliciosa haga peticiones a tu servidor de desarrollo y lea la respuesta
    • ⁉️ Puede afectar a producción? No, afecta durante el desarrollo local al dev server de esbuild o herramientas que lo usan, como Vite o Angular Devkit.
    • ❔ Dónde se encuentra: en Vite y en Angular Devkit.
  • ☑️ webpack-dev-server: Cross-Origin Source Code Leakage. Si visitas una web maliciosa mientras tienes el dev server corriendo, puede robar contenido de tu proyecto (por ejemplo, código fuente que estés sirviendo).
    • ⁉️ Puede afectar a producción? No, afecta durante el desarrollo local al dev server de esbuild o herramientas que lo usan, como Vite o Angular Devkit.
    • ❔ Dónde se encuentra: en Vite y en Angular Devkit.

Ahora ejecuta este otro comando en la terminal de tu proyecto:

npm audit --production

A mi me devolvió esto:

npm audit --production
npm warn config production Use `--omit=dev` instead.
found 0 vulnerabilities

Este "Found 0 vulnerabilities" nos comunica que en principio, hasta prueba contraria, no tenemos ninguna vulnerabilidad detectada para producción.

npm audit fix: cómo arreglárnosla si queremos hacerlo bien

Cuando ejecuto el comando

npm audit fix

Lo que hace NPM es ir recorriendo el árbol de dependencias del proyecto con el fin de actualizar las versiones de los paquetes afectados por vulnerabilidades reconocidas.

NPM intentará resolver los problemas dentro del rango de versiones compatibles con respecto a lo que definiste en tu package.json. Por ejemplo, si tienes una dependencia en la versión ^1.2.0, intentará actualizar solo dentro de la misma versión, o hacia abajo. Con un npm fix --force podemos ir forzando las actualizaciones más allá de esta restricción, pero es bastante desaconsejado, porque podría comprometer el funcionamiento de nuestros componentes.

Muchas veces NPM no es capaz de arreglar todas las vulnerabilidades por una razón muy simple: Las correcciones de seguridad a veces solo existen en versiones más recientes o mayores, que romperían la compatibilidad con la versión de tu proyecto. Esto pasa especialmente en proyectos grandes o antiguos donde las dependencias tienen un encaje muy específico.

Entonces, ¿Cómo resuelvo todas mis vulnerabilidades? La respuesta rápida sería: Actualizar tu proyecto y todas sus dependencias a versiones más recientes, hasta que desaparezcan los warnings. Pero claro… eso no siempre es viable de forma inmediata. Cada actualización mayor trae consigo riesgo de breaking changes y posibles errores inesperados en tu aplicación.

Lo ideal, en realidad, suele ser

  • 📌 Analizar el alcance real de las vulnerabilidades (¿afectan solo a desarrollo o también a producción?).
  • 📌 Evaluar el riesgo real de cada vulnerabilidad (no todas tienen la misma gravedad ni impacto real en tu caso de uso).
  • 📌 Actualizar de forma controlada, primero las que afectan a producción.
  • 📌 Hacer pruebas exhaustivas después de cada actualización significativa.

La verdadera mejor práctica: Replantearte cuántas dependencias necesitas

Más allá de hacer "fixes" a ciegas, lo ideal a largo plazo es:

  • ✅ Reducir al mínimo las dependencias de terceros en tu proyecto.
  • ✅ No instalar un paquete solo por pereza o por ahorrarte unas líneas de código: a veces importamos un paquete completo de NPM para hacer algo que podríamos haber implementado en 5-10 líneas de código propias, sin dependencias externas.

Cada dependencia que añades es un vector potencial de vulnerabilidades futuras, además de:

  • ⛔ Incrementar el tamaño del proyecto.
  • ⛔ Aumentar los tiempos de build.
  • ⛔ Generar conflictos en futuras actualizaciones.

Menos dependencias = menos superficie de ataque + menos mantenimiento a largo plazo. El enfoque maduro no es solo hacer npm audit fix, sino tener una estrategia consciente de gestión de dependencias.

Este artículo ha sido fruto de una investigación personal sobre el tema, motivada por un debate al que asistí recientemente. No me considero una experta en ciberseguridad, pero creo que las historias y curiosidades que he ido descubriendo a lo largo de este proceso merecen ser compartidas. Sobre todo con quienes, como yo, utilizan NPM a diario y a veces, entre prisas y builds, dejan pasar de largo ese mensajito de la terminal: “n VULNERABILITIES FOUND”.

Si eres experto en la materia y quieres hacerme llegar alguna corrección, comentario o simplemente compartir tu punto de vista, estaré encantada de leerte en mi bandeja de entrada: contacto@giuliacapozzi.com.