Une API Express nommée api-service peut réussir ses tests, générer une image OCI et pourtant inclure du code que personne n’avait l’intention d’approuver. Un développeur ajoute expresss au lieu de express dans le fichier package.json ; le registre public résout le paquet au nom similaire, l’exécuteur GitHub Actions l’installe et l’image finale le contient. L’attaquant n’a eu besoin ni d’un accès en écriture au dépôt, ni d’une clé de déploiement volée, ni d’un accès au cluster de production. La question qui se pose pour le défenseur est plus ciblée et plus utile : comment empêcher du code non examiné de s’intégrer à une build via la résolution des dépendances ? Nous allons suivre le parcours de api-service, depuis son manifeste jusqu’à son image publiée, en passant par sa build CI.
Le processus de build franchit plusieurs frontières de confiance avant que l’image ne soit déployée. Un registre de paquets public fournit le code à partir d’un nom indiqué dans un manifeste, l’exécuteur CI procède à l’installation et le registre d’artefacts stocke l’image résultante en vue d’un déploiement ultérieur. Un paquet malveillant peut tenter d’exécuter du code durant la build, introduire une dépendance transitive nuisible ou dérober un jeton de publication accessible à l’exécuteur. Le schéma suivant met en évidence ces frontières :
Ce schéma illustre pourquoi le choix des dépendances relève de la gestion de la chaîne d’approvisionnement logicielle plutôt que du simple codage local. Une fois qu’un paquet est récupéré par l’exécuteur, son contenu intègre les données d’entrée de la phase de construction (build) et peut déclencher l’exécution de scripts d’installation, à moins que le pipeline ne restreigne cette possibilité.
Le fichier manifeste constitue la première ligne de défense, car la commande npm install interprète chaque nom de paquet déclaré comme une instruction visant à localiser et télécharger du code. Dans le cas de api-service, la version attendue de express est la 4.18.3, alors que le nom inventé expresss (version 1.0.0) résulte d’une faute de frappe désignant un paquet distinct. Il s’agit d’une attaque par typosquatting : un attaquant publie un paquet dont le nom ressemble à celui d’une dépendance légitime, en comptant sur une erreur humaine pour qu’il soit sélectionné.
Un fichier de verrouillage (lockfile) consigne la version résolue du paquet, les métadonnées du registre et l’empreinte d’intégrité pour un graphe de dépendances donné. Cet enregistrement garantit la reproductibilité des installations ultérieures, mais seulement une fois que le paquet erroné a déjà été accepté et validé (commit). L’échec de la révision est donc visible à deux niveaux : le fichier package.json contient une dépendance directe inattendue, tandis que le fichier package-lock.json inclut une nouvelle entrée résolue ainsi qu’une valeur d’intégrité associée. Quels nom de paquet et entrée dans le fichier de verrouillage devraient entraîner le rejet de l’ajout de expresss lors de la révision ?
Cet exemple de code démontre qu’une installation réussie ne garantit pas que la dépendance prévue a bien été sélectionnée. L’outil d’installation a résolu expresss exactement comme demandé, puis a consigné le résultat dans le fichier de verrouillage (lockfile) ; la répétition déterministe de cette erreur reste une erreur. Examiner les différences entre les dépendances revient à considérer un nom inconnu, une version inattendue ou une nouvelle entrée d’intégrité comme du code entrant dans la version publiée.
Règle d’examen Un fichier de verrouillage atteste de ce que l’outil d’installation a sélectionné. Il ne prouve pas que cette sélection était autorisée ou sûre.
Si une faute de frappe dans un nom public est à l’origine du problème dans le manifeste, un paquet privé peut être remplacé même si son nom est correct. La confusion de dépendances (dependency confusion) survient lorsqu’une procédure de construction (build) résout un paquet à partir d’un registre non prévu ; cela se produit généralement lorsqu’un paquet public portant le même nom, ou une version correspondante plus récente, supplante un paquet interne. L’attaquant doit simplement pouvoir publier sur un registre consulté par la procédure de construction, sans avoir besoin d’accéder au dépôt de code source privé.
Pour api-service, une dépendance interne telle que @acme/auth-client doit toujours être résolue à partir du registre privé de l’organisation. Une plage de versions étendue autorise l’utilisation de versions futures respectant cette plage, tandis que la priorité des registres détermine l’emplacement où l’outil d’installation effectue sa recherche. Bien que le fichier de verrouillage fige le résultat pour une construction validée, il ne peut corriger une configuration de registre orientant une nouvelle construction vers la mauvaise source, ni protéger contre une mise à jour délibérée du fichier de verrouillage qui échapperait à la vigilance des relecteurs. L’objectif de contrôle est d’assurer une résolution déterministe depuis le registre prévu pour chaque portée (scope) de paquet.
Cette comparaison distingue la frontière franchie par chaque menace des éléments permettant de la détecter :