Skip to main content

Detecta y evita ataques de confusión de dependencias en npm para mantener la seguridad de la cadena de suministro

Escrito por

13 de septiembre de 2021

0 minutos de lectura

El 9 de febrero de 2021, Alex Birsan dio a conocer su investigación de seguridad, acertadamente llamada confusión de dependencias. En su divulgación, describe cómo un nuevo ataque a la cadena de suministro, que aprovecha errores de configuración de los desarrolladores y fallas de diseño de numerosos administradores de paquetes en ecosistemas de software de código abierto basados en lenguajes de programación, le permitió obtener acceso y exfiltrar datos de empresas como Yelp, Tesla, Apple, Microsoft y otras.

Esta investigación de seguridad despertó interés y, con ello, dio lugar a una nueva variedad de herramientas para ayudar a las organizaciones a detectar si son vulnerables o están expuestas a posibles ataques de confusión de dependencias.

En este artículo, te explicaré paso a paso en qué consiste el ataque de confusión de dependencias y cómo afecta a los desarrolladores de JavaScript y Node.js que trabajan en el ecosistema npm. También veremos una nueva herramienta de código abierto de Snyk que te permite detectar posibles riesgos de confusión de dependencias en tus propios repositorios de código fuente: snync.

¡Pregunta rápida! ¿Adivinas qué significa snync? La respuesta está al final...

En este artículo, conoceremos las distintas formas en que se manifiesta este ataque a la cadena de suministro y detallaremos cómo mitigar cada una:

  • Error de configuración del registro npm privado

  • El registro npm privado obtiene las versiones más recientes

  • Las actualizaciones manuales de paquetes pueden introducir versiones maliciosas

¿Te afectan los ataques de confusión de dependencias?

El ataque de confusión de dependencias solo afecta a las organizaciones que dependen de bibliotecas de código fuente internas. Es muy común administrar paquetes privados para proteger la propiedad intelectual, por lo que muchas organizaciones utilizan proxies internos, cachés o servicios de alojamiento de paquetes privados para hacerlo.

Si una organización administra un paquete privado interno, por definición, este no estará disponible en los registros públicos ni en sus réplicas. Como el paquete privado no figura en un registro público, cualquier otra persona puede reservar ese nombre y potencialmente lanzar un ataque de confusión de dependencias contra ti. El hecho de que un paquete privado pueda tener el mismo nombre que uno público es la base de este ataque.

En los ecosistemas de JavaScript y Node.js, la superficie de ataque de la confusión de dependencias se reduce considerablemente si usas paquetes con ámbito como espacio de nombres reservado. Pero ten en cuenta que, tanto si usas npm como yarn, estás expuesto a este ataque a la cadena de suministro.

Cómo reproducir ataques de confusión de dependencias

Para explorar de forma práctica los ataques a la cadena de suministro que toman la forma de confusión de dependencias, haremos un tutorial práctico que demostrará la vulnerabilidad y cómo mitigarla.

A continuación se muestra el manifiesto de paquetes de un proyecto conocido internamente como la Estrella de la Muerte. ¡Un gran nombre, lo sé!

{
  "name": "the-death-star",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
  },
  "license": "ISC",
  "dependencies": {
    "debug": "^4.3.2",
    "death-star-secret-hyper-matter-reactor": "^1.0.0",
    “superlaser”: “^1.0.0”
  }
}

Este código se encuentra en package.json y también muestra las dependencias del proyecto. Como puedes ver, superlaser es uno de los paquetes de los que depende. Por supuesto, se trata de un arma muy secreta que posee el imperio, por lo que solo se publica de forma interna. La Estrella de la Muerte debe ser móvil en el espacio y, por ello, también depende del paquete privado death-star-secret-hyper-matter-reactor.

Cómo configurar un registro npm privado

Si quieres seguir los pasos desde casa, haremos una breve pausa en el tema principal del artículo para configurar un registro npm privado. Con un registro privado, puedes hacer pruebas por tu cuenta y reproducir de principio a fin este ataque de confusión de dependencias en la cadena de suministro.

Para hacerlo, configuraremos un registro npm privado interno y un servidor proxy con Verdaccio, un proyecto de código abierto creado para este fin. Si tienes Docker instalado, podemos ponerlo en marcha fácilmente así:

docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio\

Si todo sale bien, Verdaccio nos dará la bienvenida:

Salida de terminal que muestra un comando de Docker que inicia Verdaccio en el puerto 4873, con los complementos cargados y la dirección HTTP en pantalla

¡Felicitaciones! Ahora tienes tu propio alojamiento dedicado de paquetes npm ejecutándose localmente en el puerto 4873.

Publiquemos nuestros paquetes npm internos y secretos superlaser y death-star-secret-hyper-matter-reactor. Primero, agregaremos un usuario a este nuevo registro privado de Verdaccio:

npm adduser --registry https://localhost:4873

A continuación, veremos el archivo de manifiesto del paquete superlaser, package.json:

{
  "name": "superlaser",
  "version": "1.0.0",
  "description": "Our secrets weapon",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
  },
  "license": "ISC"
}

El manifiesto del paquete death-star-secret-hyper-matter-reactor es similar, solo cambia el nombre del paquete. Publiquémoslo en nuestro registro npm privado:

npm publish  --registry https://localhost:4873

¿Por qué existe la confusión de dependencias?

Hay principalmente tres situaciones que pueden dar lugar a este tipo de ataques a la cadena de suministro de software:

  • Error de configuración en un servidor de desarrollo o de pruebas

  • Publicación de versiones más recientes de paquetes en el registro npm público

  • Posibles fallas de diseño en los administradores de paquetes

Veamos de nuevo cada una de estas situaciones.

Error de configuración del registro npm privado

Cuando un desarrollador o un sistema de integración continua (CI) clona el código fuente del the-death-star project, que tiene la dependencia interna superlaser, ¿cómo obtiene esta dependencia?

Al ejecutar el comando npm install, probablemente deba cumplir estos requisitos:

  1. Necesita la URL del registro npm privado donde se encuentra este paquete interno.

  2. Necesita un token o algún tipo de credenciales para acceder a ese registro privado.

El primer paso mencionado es donde algo puede salir mal. Para especificar un registro npm privado en particular, es necesario proporcionar explícitamente la información de configuración del administrador de paquetes npm.

Veamos ahora algunas situaciones:

  1. ¿Qué pasa si el sistema de integración continua no tiene configurado el registro privado?

  2. ¿Qué pasa si eres un nuevo desarrollador que se incorpora a un proyecto existente y no realizaste pasos previos, como ejecutar el comando npm config set registry?

  3. ¿Qué pasa si por error eliminaste o modificaste tu configuración .npmrc y ya no incluye el registro npm privado interno?

En cualquiera de estos casos, si se omite la configuración personalizada del registro interno, el administrador de paquetes npm usará de forma predeterminada el registro público (registry.npmjs.org) y descargará paquetes de ahí.

Cualquiera puede publicar paquetes en el registro npm público. Por eso, si un usuario malicioso publicara un paquete llamado superlaser, este se descargaría e instalaría en lugar de tu propio paquete interno.

Cómo protegerte contra la confusión de dependencias en npm

El problema central es no tener configurado correctamente el proxy npm privado. Si un desarrollador o un sistema de CI no cuenta con esta configuración, podrías estar expuesto.

Por eso, el primer paso es: Asegúrate siempre de que haya un archivo .npmrc disponible o de contar con otro tipo de configuración para el proxy npm privado.

En segundo lugar, puedes adoptar un enfoque proactivo para detectar casos en los que usas paquetes privados cuyo espacio de nombres no está reservado en el registro npmjs público. Creamos snync para ayudarte con eso. Puedes ejecutarlo en un servidor de CI como parte de los pasos, antes de instalar activamente las dependencias. Así, puede evitar que instales por error un paquete malicioso.

En la siguiente captura de pantalla, ejecuto snync mediante npx e indico el directorio actual para analizar las dependencias. También especifico que el paquete llamado superlaser es efectivamente un paquete privado:

Salida de la terminal que muestra a snyc analizando dependencias; una aparece como vulnerable y otra como sospechosa.

Como puedes ver en los resultados, snync confirmó dos posibles problemas específicos relacionados con la confusión de dependencias:

  1. El paquete death-star-secret-hyper-matter-reactor es vulnerable porque actualmente no hay ningún paquete con ese nombre registrado en el registro npmjs público. Esto significa que cualquiera puede registrarlo y provocar un ataque de confusión de dependencias.

  2. El paquete superlaser es suspicious. Esto significa que la herramienta detectó uno de estos dos casos:

    1. Este nombre de paquete se introdujo primero en el código fuente de Git y, más adelante, se publicó en el registro npmjs público un paquete con el mismo nombre. Esto no significa que el paquete público en el registro npmjs sea malicioso, pero sí amerita una revisión.

    2. El nombre del paquete ya existe en el registro npmjs público, incluso antes de que crearas un paquete privado con el mismo nombre.

snync es un proyecto de línea de comandos de código abierto basado en Node.js. Te invitamos a usarlo en tu flujo de seguridad DevSecOps.

El registro npm privado obtiene las versiones más recientes

¿Qué pasa si hay un paquete con el mismo nombre que el nuestro (superlaser) publicado y disponible en el registro npm público, pero con una versión semver superior?

La situación es la siguiente:

Ahora bien, ¿qué pasa si se crea un proyecto nuevo y se solicita instalar el paquete superlaser? Todavía no hay ningún package.json ni archivo de bloqueo (package-lock.json). El desarrollador simplemente empieza con:

npm install superlaser

Esto podría terminar instalando una versión maliciosa de superlaser controlada por un atacante remoto. Pero ¿por qué? El desarrollador tiene configurado el registro npm local.

Las pruebas demuestran que, incluso cuando está configurado un proxy npm privado interno, muchos de estos proxies primero comprueban cuál es la versión más reciente disponible en el registro npm público. Si existe una versión más nueva, obtienen del registro público la versión semver más reciente del paquete y la instalan.

Reproduzcamos esta situación con Verdaccio. Como puedes ver a continuación, subí el inofensivo paquete npm superlaser a Verdaccio, que me sirve para alojar internamente paquetes npm privados:

Página oscura del navegador Verdadccia que muestra el paquete “superlaser”, versión 1.0.0, con detalles de publicación y controles de inicio de sesión

A continuación, te mostraré cómo, en el directorio de un proyecto nuevo que solo tiene el archivo .npmrc apuntando al registro local de Verdaccio, el comando npm install para el paquete superlaser obtiene la versión más reciente del registro npm público, aunque esperaba recibir solo superlaser@1.0.0, que es la versión que publiqué internamente:

Terminal que muestra la instalación de superlaser con npm, un mensaje de postinstalación y una advertencia para el paquete the-death-star.

Esto produce un resultado inesperado y puede poner en riesgo a los usuarios finales.

Si quieres participar en la conversación y seguir este tema, hay un debate público sobre este comportamiento en el proyecto de código abierto Verdaccio en GitHub.

Técnicamente, este proceso que usan Verdaccio y otros proxies npm privados toma en cuenta varias variables, como:

  1. La versión semver más reciente

  2. La fecha de publicación del paquete

Por ejemplo, si existe una versión semver alta en el registro npmjs público, pero se crea un paquete con el mismo nombre y una versión semver inferior (dentro del mismo rango que la versión pública) después de la fecha de publicación de la versión superior, Verdaccio no obtendrá el paquete del registro público.

Cómo evitar obtener el paquete equivocado

Configura tu proxy npm privado para que nunca envíe solicitudes al registro público. Si un paquete o una versión no está disponible localmente, debe resolverse de una forma que no obtenga paquetes a ciegas de fuentes no confiables ni verificadas.

Si usas Verdaccio, como en estos ejemplos, puedes configurarlo de la siguiente manera en /verdaccio/conf/config.yaml:

#
# This is the config file used for the docker images.
# It allows all users to do anything, so don't use it on production systems.
#
# Do not configure host and port under `listen` in this file
# as it will be ignored when using docker.
# see https://verdaccio.org/docs/en/docker#docker-and-custom-port-configuration
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage/data
# path to a directory with plugins to include
plugins: /verdaccio/plugins

web:
  # WebUI is enabled as default, if you want disable it, just uncomment this line
  #enable: false
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # darkMode: true

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

auth:
  htpasswd:
    file: /verdaccio/storage/htpasswd
    # Maximum amount of users allowed to register, defaults to "+infinity".
    # You can set this to -1 to disable registration.
    # max_users: 1000

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

  '**':
    # allow all users (including non-authenticated users) to read and
    # publish all packages
    #
    # you can specify usernames/groupnames (depending on your auth plugin)
    # and three keywords: "$all", "$anonymous", "$authenticated"
    access: $all

    # allow all known users to publish/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

    # if package is not available locally, proxy requests to 'npmjs' registry
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

middlewares:
  audit:
    enabled: true

# log settings
logs:
  - { type: stdout, format: pretty, level: http }
  #- {type: file, path: verdaccio.log, level: info}
#experiments:
#  # support for npm token command
#  token: false
#  # support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
#  search: false

# This affect the web and api (not developed yet)
#i18n:
#web: en-US

Lo anterior es el archivo de configuración estándar de Verdaccio para la versión del contenedor de Docker. Sin embargo, puedes ubicar el comentario # DO NOT FETCH PACKAGES FROM NPMJS, que comenta en la línea siguiente la opción proxy: npmjs. Esto impide que Verdaccio obtenga nada de npmjs para los paquetes que coincidan con el patrón indicado.

Las actualizaciones manuales de paquetes pueden introducir versiones maliciosas

En este escenario, actualizas manualmente tus paquetes de npm ejecutando npm update o npm install <packages>@latest para actualizar las versiones de tus dependencias.

Al ejecutar estos procedimientos de actualización, ocurre aquí el mismo comportamiento que vimos antes. El comando npm update le pide al proxy privado de npm que obtenga la versión más reciente y, a su vez, el proxy consulta cuál es la versión más actualizada en el registro público de npm.

Ten en cuenta que, si usas yarn, ejecutar yarn upgrade dará el mismo resultado: descargará paquetes potencialmente maliciosos del registro público npmjs.

Podemos demostrarlo con el siguiente escenario, en el que empezamos con la versión interna superlaser@1.0.0:

{
  "name": "new-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "superlaser": "^1.0.0"
  }
}

Ahora, tengo un archivo .npmrc que define el registro local y apunta al servidor de Verdaccio que tengo en ejecución. Sin embargo, si simplemente ejecuto npm update para actualizar todas mis dependencias, puedes ver que descarga la versión más reciente que coincide con semver desde el registro público npmjs:

Salida de la terminal que muestra npm update instalando superlaser@1.99999.999 y mostrando un mensaje de postinstall y los resultados de la auditoría

¿Cómo protegerte?

En lugar de actualizar manualmente los paquetes de npm a ciegas, opta por las actualizaciones automatizadas de paquetes mediante pull requests dirigidos a los repositorios de tus proyectos de código abierto, que también se encargarán de sincronizar el manifiesto del paquete (como package-lock.json o yarn.lock).

Snyk es una opción gratuita para automatizar las actualizaciones de paquetes de npm, como muestra la siguiente captura de pantalla de un pull request combinado:

pull request de GitHub que muestra cómo Snyk actualiza node-uuid de la versión 1.4.0 a la 1.4.8 para corregir un problema de vector inseguro de gravedad media.

También puedes ajustar las opciones de actualización automática, por ejemplo, limitar la cantidad de pull requests que abrirá Snyk o ignorar por completo la actualización de paquetes específicos. Encontrarás más información en la documentación de Snyk sobre cómo actualizar dependencias con PR automáticos.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Para concluir: recursos sobre seguridad de aplicaciones

Si llegaste hasta aquí, te ganaste la respuesta al cuestionario que presentamos al inicio del artículo. Llamamos a la herramienta de código abierto snync como abreviatura de So Now You’re Not Confused. ¿Lo entendiste?

Hoy, ante los incidentes de seguridad de la cadena de suministro, es fundamental aplicar metodologías seguras, tanto al escribir código como en las prácticas diarias de seguridad para desarrolladores. Si tú o tu equipo trabajan habitualmente con JavaScript o Node.js, estos recursos de referencia les serán útiles:

  1. 10 prácticas recomendadas de seguridad para GitHub

  2. 10 prácticas recomendadas de seguridad para npm

  3. 10 prácticas recomendadas para contenerizar aplicaciones web de Node.js con Docker