Skip to main content

Détecter et prévenir les attaques par confusion de dépendances sur npm pour garantir la sécurité de la chaîne logistique logicielle

Écrit par

13 septembre 2021

0 minutes de lecture

Le 9 février 2021, Alex Birsan a révélé ses recherches en sécurité, judicieusement intitulées dependency confusion. Il y décrit comment une nouvelle attaque de la chaîne logistique logicielle, exploitant les erreurs de configuration des développeurs ainsi que les défauts de conception de nombreux gestionnaires de paquets dans les écosystèmes logiciels open source fondés sur des langages de programmation, lui a permis d’accéder à des données d’entreprises telles que Yelp, Tesla, Apple, Microsoft et d’autres, puis de les exfiltrer.

Ces recherches en sécurité ont attiré l’attention et fait émerger un nouvel éventail d’outils permettant aux organisations de détecter si elles sont vulnérables ou exposées à d’éventuelles attaques par confusion de dépendances.

Dans cet article, je vais vous présenter pas à pas l’attaque par confusion de dépendances et ses manifestations pour les développeurs JavaScript et Node.js de l’écosystème npm. Nous découvrirons également un nouvel outil open source de Snyk qui vous permet de détecter les risques potentiels liés à la confusion de dépendances dans vos propres dépôts de code source : snync.

Petite question ! À votre avis, que signifie snync ? Réponse à la fin…

Dans cet article, nous allons découvrir chacune des façons dont cette attaque de la chaîne logistique logicielle se manifeste et détailler les moyens de les atténuer :

  • Mauvaise configuration d’un registre npm privé

  • Le registre npm privé récupère les versions les plus récentes

  • Les mises à jour manuelles de paquets peuvent introduire des versions malveillantes

Les attaques par confusion de dépendances vous concernent-elles ?

L’attaque par confusion de dépendances ne concerne que les organisations qui s’appuient sur des bibliothèques de code source internes. La gestion de paquets privés pour protéger la propriété intellectuelle est très répandue. De nombreuses organisations utilisent donc des proxys internes, des caches ou des services de registre privés pour héberger leurs paquets.

Si une organisation gère un paquet privé interne, celui-ci n’existe (par définition) pas dans les registres publics ni leurs miroirs. Comme ce paquet privé n’est pas répertorié dans un registre public, n’importe qui peut réserver son nom et potentiellement lancer contre vous une attaque par confusion de dépendances. Le fait qu’un paquet privé puisse porter le même nom qu’un paquet public est au cœur de cette attaque.

Dans les écosystèmes JavaScript et Node.js, le risque d’attaque par confusion de dépendances est fortement réduit si vous utilisez des paquets à portée délimitée comme espace de noms réservé. Notez toutefois que, que vous utilisiez npm ou yarn, vous restez exposé à cette attaque de la chaîne logistique logicielle.

Reproduire les attaques par confusion de dépendances

Pour étudier concrètement les attaques de la chaîne logistique logicielle prenant la forme d’une confusion de dépendances, nous allons suivre un tutoriel pratique qui montre la vulnérabilité et comment l’atténuer.

Voici le manifeste de paquets d’un projet connu en interne sous le nom d’Étoile de la Mort. Joli nom, n’est-ce pas ?

{
  "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”
  }
}

Ce code se trouve dans le fichier package.json, qui répertorie également les dépendances de ce projet. Comme vous pouvez le voir, superlaser est une dépendance. Bien sûr, il s’agit d’une arme secrète de l’Empire, elle est donc publiée uniquement en interne. L’Étoile de la Mort doit pouvoir se déplacer dans l’espace et s’appuie donc aussi sur le paquet privé death-star-secret-hyper-matter-reactor.

Configurer un registre npm privé

Si vous souhaitez suivre l’exercice chez vous, nous devons nous écarter brièvement du sujet principal pour configurer un registre npm privé. Avec un registre privé, vous pourrez faire vos propres essais et reproduire de bout en bout cette attaque de la chaîne logistique logicielle par confusion de dépendances.

Pour cela, nous allons configurer un registre npm privé interne et un serveur proxy à l’aide de Verdaccio, un projet open source conçu à cet effet. Si Docker est installé, nous pouvons le lancer très facilement comme suit :

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

Si tout se passe bien, Verdaccio nous accueillera alors :

Sortie du terminal montrant une commande Docker qui lance Verdaccio sur le port 4873, avec les plugins chargés et l’adresse HTTP affichée

Félicitations ! Vous disposez maintenant de votre propre hébergement dédié de paquets npm, opérationnel en local sur le port 4873.

Publions maintenant nos paquets npm internes secrets, superlaser et death-star-secret-hyper-matter-reactor. Commençons par ajouter un utilisateur à ce nouveau registre privé Verdaccio :

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

Passons ensuite au manifeste de notre paquet superlaser, dans le fichier package.json :

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

Le manifeste du paquet death-star-secret-hyper-matter-reactor est similaire ; seul le nom du paquet change. Publions-le maintenant dans notre registre npm privé :

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

Pourquoi la confusion de dépendances existe-t-elle ?

Trois situations peuvent principalement conduire à ce type d’attaques de la chaîne logistique logicielle :

  • Une mauvaise configuration sur un serveur de développement ou de test

  • La publication de versions plus récentes de paquets dans le registre npm public

  • Éventuellement, des défauts de conception dans les gestionnaires de paquets

Examinons à nouveau chacun de ces cas.

Mauvaise configuration d’un registre npm privé

Lorsqu’un développeur ou un système d’intégration continue (CI) clone le code source du the-death-star project, qui dépend en interne de superlaser, comment obtient-il cette dépendance ?

Lorsqu’une commande npm install est exécutée, il doit probablement remplir les critères suivants :

  1. Il lui faut l’URL du registre npm privé où se trouve ce paquet interne.

  2. Il lui faut un jeton ou des identifiants pour accéder à ce registre privé.

C’est à la toute première étape décrite ci-dessus que les choses peuvent mal tourner. Pour indiquer un registre npm privé particulier, il faut fournir explicitement les informations de configuration au gestionnaire de paquets npm.

Examinons maintenant quelques scénarios :

  1. Que se passe-t-il si le système d’intégration continue n’est pas configuré avec le registre privé ?

  2. Que se passe-t-il si vous êtes un nouveau développeur qui rejoint un projet existant et que vous n’avez pas effectué les étapes préalables, comme exécuter la commande npm config set registry ?

  3. Que se passe-t-il si vous avez supprimé ou modifié par erreur votre configuration .npmrc et qu’elle n’inclut plus le registre npm privé interne ?

Dans tous ces cas, si le paramètre personnalisé du registre interne est absent, le gestionnaire de paquets npm utilise par défaut le registre public (registry.npmjs.org) et y télécharge les paquets.

N’importe qui peut publier des paquets dans le registre npm public. Si un utilisateur malveillant y publie un paquet nommé superlaser, celui-ci sera téléchargé et installé à la place de votre paquet interne.

Comment se protéger contre la confusion de dépendances sur npm

Le problème principal vient de l’absence d’une configuration adéquate du proxy npm privé. Si un développeur ou un système CI ne dispose pas de cette configuration, vous êtes potentiellement vulnérable.

La première étape consiste donc à toujours veiller à ce qu’un fichier .npmrc soit disponible, ou à prévoir une autre forme de configuration du proxy npm privé.

Ensuite, vous pouvez adopter une approche proactive pour détecter les cas où vous utilisez des paquets privés dont l’espace de noms n’est pas réservé dans le registre public npmjs. Nous avons créé snync pour vous aider. Vous pouvez l’exécuter sur un serveur CI dans le cadre des étapes précédant l’installation des dépendances. Vous éviterez ainsi d’installer par erreur un paquet malveillant.

Dans la capture d’écran suivante, j’exécute snync vianpx et lui indique le répertoire courant à analyser à la recherche de dépendances. Je précise également que le paquet nommé superlaser est bien un paquet privé :

Sortie du terminal montrant l’analyse des dépendances par snyc, dont une est signalée comme vulnérable et une autre comme suspecte.

Comme vous pouvez le voir dans les résultats, snync m’a signalé deux cas précis de risques potentiels de confusion de dépendances :

  1. Le paquet death-star-secret-hyper-matter-reactor est vulnerable, car aucun paquet portant ce nom n’est actuellement enregistré dans le registre public npmjs. Cela signifie que n’importe qui peut l’enregistrer et déclencher ainsi une attaque par confusion de dépendances.

  2. Le paquet superlaser est suspicious. Cela signifie que l’outil a détecté l’un des deux cas suivants :

    1. Ce nom de paquet a d’abord été introduit dans le code source Git, puis, plus tard, un paquet du même nom a été publié dans le registre public npmjs. Cela ne signifie pas que le paquet public sur npmjs est malveillant, mais il convient de l’examiner.

    2. Le nom du paquet existe déjà dans le registre public npmjs, avant même que vous ne créiez un paquet privé du même nom.

snync est un outil en ligne de commande open source basé sur Node.js. Nous vous invitons à l’utiliser dans votre pipeline de sécurité DevSecOps.

Le registre npm privé récupère les versions les plus récentes

Que se passe-t-il si un paquet du même nom que le nôtre (superlaser) est publié et disponible dans le registre npm public, mais possède une version semver supérieure ?

Voici la situation :

Que se passe-t-il si un nouveau projet est créé et qu’il demande l’installation du paquet superlaser ? Il n’y a pas encore de package.json, ni de fichier de verrouillage (package-lock.json). Le développeur commence simplement par :

npm install superlaser

Cette installation peut aboutir à une version malveillante de superlaser, contrôlée par un attaquant à distance. Mais pourquoi ? Le développeur a pourtant configuré le registre npm local.

Comme le montrent les tests, même lorsqu’un proxy npm privé interne est configuré, on a observé que beaucoup de ces proxys vérifient d’abord la version la plus récente disponible dans le registre npm public. Si une version plus récente existe, ils récupèrent la version semver la plus récente du paquet depuis le registre public et l’installent.

Reproduisons ce scénario avec Verdaccio. Comme vous pouvez le voir ci-dessous, j’ai envoyé le paquet npm inoffensif superlaser à Verdaccio, qui me sert d’hébergement interne pour les paquets npm privés :

Page sombre du navigateur Verdadccia affichant le package « superlaser », version 1.0.0, avec les détails de publication et les options de connexion

Je vais ensuite vous montrer comment, dans un nouveau répertoire de projet contenant uniquement le fichier .npmrc pointant vers le registre Verdaccio local, une commande npm install pour le paquet superlaser récupère la version la plus récente du registre npm public. Je m’attendais pourtant à obtenir uniquement superlaser@1.0.0, la version que j’ai publiée en interne :

Terminal affichant l’installation de superlaser avec npm, un message postinstall et un avertissement concernant le package the-death-star.

Cela produit un résultat inattendu et peut potentiellement mettre les utilisateurs finaux en danger.

À noter : une discussion publique est en cours sur GitHub, au sein du projet open source Verdaccio, si vous souhaitez y participer et suivre le sujet.

Techniquement, le processus utilisé par Verdaccio et d’autres proxys npm privés tient compte de plusieurs variables, notamment :

  1. La dernière version semver

  2. La date de publication du paquet

Par exemple, si une version semver élevée existe dans le registre public npmjs, mais qu’un paquet du même nom avec une version semver inférieure (dans la même plage que la version publique) est créé après la date de publication de la version supérieure, Verdaccio ne récupérera pas le paquet du registre public.

Comment éviter de récupérer le mauvais paquet

Configurez votre proxy npm privé pour qu’il ne relaie jamais les requêtes vers les registres publics. Si un paquet ou une version n’est pas disponible localement, il doit être résolu d’une manière qui n’entraîne pas la récupération automatique de paquets issus de sources non fiables et non vérifiées.

Si vous utilisez Verdaccio, comme dans nos exemples, vous pouvez procéder ainsi à l’aide de la configuration suivante, située dans /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

Il s’agit du fichier de configuration Verdaccio fourni par défaut avec la version conteneur Docker, à ceci près que vous pouvez repérer le commentaire # DO NOT FETCH PACKAGES FROM NPMJS, qui désactive à la ligne suivante l’option proxy: npmjs. Cela empêche Verdaccio de récupérer quoi que ce soit depuis npmjs pour les paquets correspondant au modèle indiqué.

Les mises à jour manuelles des paquets peuvent introduire des versions malveillantes

Dans ce scénario, vous mettez manuellement à jour vos paquets npm en exécutant npm update ounpm install <packages>@latest pour mettre à jour les versions de vos dépendances.

Lorsque vous lancez ces procédures de mise à jour, le même phénomène que précédemment se produit. La commande npm update demande au proxy npm privé de récupérer la dernière version, puis le proxy vérifie quelle est la version la plus récente dans le registre npm public.

À noter : si vous utilisez Yarn, l’exécution de yarn upgrade aura le même résultat et récupérera des paquets potentiellement malveillants depuis le registre npmjs public.

Voici un scénario qui l’illustre : nous commençons avec la version interne 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"
  }
}

J’ai un fichier .npmrc qui définit le registre local et pointe vers le serveur Verdaccio que j’ai lancé. Pourtant, si j’exécute simplement npm update pour mettre à jour toutes mes dépendances, vous pouvez constater qu’il récupère la dernière version correspondant à la plage semver depuis le registre npmjs public :

Sortie du terminal montrant la mise à jour de npm qui installe superlaser@1.99999.999 et affiche un message postinstall ainsi que les résultats de l’audit

Comment s’en protéger ?

Au lieu de mettre à jour manuellement et à l’aveugle vos paquets npm, optez pour des mises à jour automatisées sous forme de pull requests soumises aux dépôts de vos projets open source. Cette approche permet également de synchroniser le manifeste des paquets (comme package-lock.json ou yarn.lock).

Snyk vous permet notamment d’automatiser gratuitement les mises à jour des paquets npm, comme le montre la capture d’écran suivante d’une pull request fusionnée :

Demande de tirage GitHub montrant que Snyk met à niveau node-uuid de la version 1.4.0 à la version 1.4.8 pour corriger une vulnérabilité de sévérité moyenne liée à un vecteur non sécurisé.

Vous pouvez affiner les paramètres de mise à jour automatisée, par exemple en limitant le nombre de pull requests que Snyk ouvrira ou en excluant complètement certains paquets des mises à jour. Pour en savoir plus, consultez la documentation Snyk sur la mise à niveau des dépendances avec des PR automatiques.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.

Pour conclure : ressources sur la sécurité des applications

Si vous êtes arrivé jusqu’ici, vous avez bien mérité la réponse au quiz présenté au début de l’article. Nous avons nommé l’outil open source snync, abréviation de So Now You’re Not Confused. Aviez-vous trouvé ?

À l’heure où les incidents touchant la sécurité de la chaîne d’approvisionnement se multiplient, on ne saurait trop insister sur l’importance d’adopter des pratiques sécurisées, que ce soit dans l’écriture du code ou dans les gestes de sécurité quotidiens des développeurs. Si vous ou votre équipe travaillez régulièrement avec JavaScript ou Node.js, ces ressources de référence vous seront utiles :

  1. 10 bonnes pratiques de sécurité GitHub

  2. 10 bonnes pratiques de sécurité npm

  3. 10 bonnes pratiques pour conteneuriser des applications web Node.js avec Docker