Cartographier la dependency confusion : une nouvelle approche de détection à partir des fichiers source map
Cet article présente les vulnérabilités de dependency confusion, introduit une technique inédite de détection et d'exploitation, puis propose des mesures concrètes pour atténuer les risques liés à cette vulnérabilité.
Introduction
La chaîne d'approvisionnement logicielle englobe tous les éléments impliqués dans le cycle de vie d'un code, du développement au déploiement, y compris le code, les binaires et les dépendances provenant de dépôts ou de gestionnaires de paquets.
Si l'utilisation de code tiers permet de gagner du temps et de l'effort, elle introduit aussi des risques de sécurité potentiels, car des failles dans ces dépendances peuvent compromettre la sécurité de toute la chaîne d'approvisionnement.
Certaines entreprises choisissent d'utiliser des dépendances internes plutôt que du code tiers, hébergées sur des registres privés ou publics avec des scopes privés, mais cette approche n'est pas à l'abri des problèmes de sécurité et peut conduire à la dependency confusion, un nouveau type de vulnérabilité de la chaîne d'approvisionnement logicielle.
La dependency confusion peut survenir dans de nombreux gestionnaires de paquets. Dans cet article, nous nous concentrons sur npm, un gestionnaire de paquets très répandu pour Javascript.
Présentation de la dependency confusion
La dependency confusion se produit lorsqu'un acteur malveillant parvient à tromper un gestionnaire de paquets (NPM, PyPI, RubyGems, JFrog Artifactory...) pour qu'il télécharge un paquet ou une dépendance malveillants au lieu de ceux qui sont légitimes. Cela peut se produire à cause de plusieurs erreurs de configuration et de mauvaises hypothèses, notamment :
Fautes de frappe
L'une des façons les plus simples de provoquer une dependency confusion est la faute de frappe : l'attaquant peut anticiper que des développeurs installeront par inadvertance le paquet malveillant à la place du paquet voulu, à cause d'erreurs typographiques ou d'un manque d'attention, généralement en publiant un paquet malveillant similaire à un paquet populaire, mais avec une faute de frappe ou une petite variation dans le nom.
Priorité des paquets
Les développeurs peuvent supposer que le gestionnaire de paquets donnera toujours la priorité aux paquets internes plutôt qu'aux paquets publics, ce qui les amène à s'appuyer sur des noms de paquets qui existent en interne mais pas sur les registres publics. Cela peut fonctionner correctement jusqu'à ce qu'un acteur malveillant découvre ce nom de paquet et décide de le revendiquer sur le registre public.
Voici un exemple de fichier package.json d'un projet javascript
{
"name": "my-project",
"version": "1.0.0",
"description": "Project 1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Corp",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"lodash": "^4.17.21",
"axios": "^0.25.0",
"moment": "^2.29.1",
"internal-corp-package": "^1.5.0"
},
"devDependencies": {
"nodemon": "^2.0.15",
"eslint": "^8.6.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.1"
}
}
Une dépendance se démarque : internal-corp-package. Son nom laisse penser qu'elle provient d'un registre interne et/ou qu'elle est destinée à un usage interne. Nous pouvons vérifier si elle existe sur le registre public.

Cela signifie que le paquet n'existe que sur un registre interne. Supposons qu'un acteur malveillant ait découvert ce paquet interne et décidé de publier un paquet malveillant du même nom sur le registre public npm : nous aurions alors le workflow suivant :

Voici le détail de la figure ci-dessus :
- Le registre n'est pas configuré explicitement
Cela signifie que l'utilisateur n'a pas explicitement indiqué à npm d'utiliser le registre interne, ni en exécutant npm config set registry http://[internal registry], ni en le spécifiant dans le fichier .npmrc. Dans ce cas, npm utilise automatiquement par défaut le registre public npm https://registry.npmjs.org/ pour l'installation, ce qui signifie qu'il installera le paquet malveillant publié par l'attaquant.
- Le registre est correctement configuré
Nous avons alors les deux scénarios suivants :
- La version du paquet spécifiée est stricte
Si la version du paquet spécifiée est stricte, comme 1.0.0, et que cette version existe sur le registre privé, npm l'utilisera automatiquement par défaut, ce qui signifie qu'il installera le paquet légitime. Voici un exemple de package.json où la version est stricte :
{
...
"dependencies": {
...
"internal-corp-package": "1.0.0"
},
...
}
- La version du paquet spécifiée est souple (plage)
Outre la version stricte, npm permet d'exprimer les versions de trois autres manières :
^1.0.0: npm installera la version 1.0.0 ou toute version supérieure à 1.0.0 et inférieure à 2.0.0, comme 1.9.5 (peut mettre à niveau la version mineure et le niveau de correctif, mais pas la version majeure).~1.0.0: npm installera la version 1.0.0 ou toute version supérieure ou égale à 1.0.0 et inférieure à 1.1.0, comme 1.0.9 (ne peut mettre à niveau que le niveau de correctif).*: aucune restriction sur la version installée ; dans ce cas, npm utilisera par défaut la dernière version.
Dans tous les cas ci-dessus, un attaquant peut tout de même mener une attaque de dependency confusion, tant qu'il parvient à obtenir la bonne version. Par exemple, si la version spécifiée est ^1.0.0, un attaquant peut créer et publier un paquet malveillant en version 1.0.1, et il passera la vérification.
Impact de la dependency confusion
La dependency confusion peut être transformée assez facilement en exécution de code à distance. L'attaquant dispose de nombreux moyens pour exécuter du code arbitraire via le paquet malveillant. L'un des plus simples consiste à utiliser la propriété scripts du fichier package.json, dans laquelle l'attaquant peut spécifier des commandes à exécuter à n'importe quelle étape de l'installation, par exemple :
{
"name": "internal-corp-package",
"version": "1.0.1",
"description": "Definitely not a malicious package!",
"main": "index.js",
"scripts": {
"postinstall": "ping [malicious host]",
},
"author": "",
"license": "ISC"
}
Nom d'organisation non revendiqué
Les registres NPM disposent d'une fonctionnalité de scope d'organisation, où le nom du paquet est de la forme @org/package. Certaines organisations enregistrent leur nom d'organisation sur le registre privé mais pas sur le registre public npm. Si un acteur malveillant parvient à revendiquer ce nom d'organisation sur le registre public npm, il peut l'utiliser pour mener une attaque de dependency confusion.
Exploiter la dependency confusion via les fichiers source map
Les fichiers source map, souvent appelés simplement « source maps », sont des fichiers qui établissent une correspondance entre le code source d'un programme et le code transformé qui est exécuté par le navigateur ou un autre moteur JavaScript. Ils sont couramment utilisés en développement web pour faciliter le débogage et la remontée d'erreurs, en particulier lorsque l'on travaille avec du code minifié ou transpilé.
Si les source maps sont utiles pour le développement et le débogage, elles peuvent avoir des implications de sécurité, en particulier lorsqu'elles sont exposées par inadvertance dans des builds de production. Si un fichier source map reste publiquement exposé dans un build de production, il peut permettre à un attaquant de reconstituer le code source original non minifié et de dresser la liste de ses dépendances, qui se trouvent généralement dans le dossier node_modules ; l'accès à la liste des dépendances peut être primordial pour mener une dependency confusion.
Un fichier source map typique généré par webpack ressemble à ceci :
{
"version": 3,
"file": "bundle.js.map",
"sources": [
"webpack:///./src/file1.ts",
"webpack:///./src/file2.ts",
"webpack:///./node_modules/internal-corp-package/utils.js"
],
"sourcesContent": [
"console.log('This is file1');",
"console.log('This is file2');",
"console.log('This is internal package utils file');"
],
"mappings": "AAAA,OAAO,EAAE;AACP,OAAO,CAAC,GAAG,EAAE",
"sourceRoot": ""
}
Cette source map nous permet de récupérer le code original non minifié de src/file.ts, src/file2.ts et node_modules/internal-corp-package/utils.js.
Dans certains cas, il était possible de trouver des dépendances intégrées directement dans le code source ; l'accès aux noms de ces dépendances pouvait lui aussi être primordial pour mener une dependency confusion.
Ce qui est encore plus inquiétant avec cette technique, c'est que l'attaquant ayant désormais accès au code original des dépendances ci-dessus, il peut injecter une porte dérobée discrète dans le code sans le casser, ce qui lui permet d'exécuter du code malveillant dans l'environnement ciblé tout en le gardant opérationnel. Cela rend la détection encore plus difficile, puisque les applications qui dépendent de ce paquet vulnérable continueront de fonctionner sans signe de compromission, contrairement au champ scripts mentionné plus haut, qui peut être facilement repéré.
Outre le risque de dependency confusion, lorsqu'un attaquant récupère le code source original d'applications javascript internes, ce code peut contenir des identifiants codés en dur, difficiles à détecter dans du code minifié, ou des commentaires qui sont supprimés dans le code transpilé.
Scan de programmes de bug bounty
Au cours de notre analyse, nous avons scanné 191 actifs issus de différents programmes de bug bounty ; environ 5 % d'entre eux se sont révélés vulnérables à la dependency confusion avec cette technique.

Se protéger contre les attaques de dependency confusion
S'il n'existe pas de solution miracle contre la dependency confusion, certaines mesures permettent d'atténuer le risque dans une certaine mesure, notamment :
- Utiliser un scope d'organisation
Les registres NPM permettent d'enregistrer un nom d'organisation et de l'utiliser comme scope. L'organisation peut être utilisée dans votre registre interne, mais vous devrez aussi l'enregistrer sur le registre public npm. Ainsi, un attaquant n'aura aucun moyen de mener une dependency confusion sans avoir accès à votre organisation.
- Enregistrer les noms des paquets internes sur le registre public npm
Une façon de garder une longueur d'avance sur les attaquants consiste à revendiquer sur le registre public npm les noms de paquets que vous utilisez en interne. Cette approche n'est toutefois pas infaillible, car vous pouvez oublier certains paquets.
- Valider l'intégrité des paquets
Chaque version de paquet npm possède un hachage sha512. Vous pouvez utiliser ce hachage pour vérifier l'intégrité du paquet avant de le télécharger.
- Épinglage des versions
Au lieu d'utiliser des versions souples comme ^1.0.0, vous pouvez utiliser la version exacte 1.0.0 ; de cette façon, NPM n'utilisera pas par défaut une version supérieure. La limite est que l'épinglage de version ne s'applique pas aux dépendances transitives.
- Verrouillage des paquets
Utilisez les fichiers package-lock.json ou yarn.lock pour verrouiller les versions précises des dépendances utilisées dans votre projet. Cela évite les changements inattendus de versions de dépendances lors des installations, en verrouillant à la fois les dépendances directes et transitives.
Conclusion
En conclusion, la dependency confusion est devenue une préoccupation pressante, susceptible de compromettre la sécurité de chaînes d'approvisionnement logicielles entières ; mettre en place des mesures pour s'en protéger est donc devenu primordial.
Tags :
supply chain