Aller au contenu

Maîtriser le risque des dépendances transitives (npm, pip, Maven, Go)

Maîtriser le risque des dépendances transitives (npm, pip, Maven, Go)

Section intitulée « Maîtriser le risque des dépendances transitives (npm, pip, Maven, Go) »

Une dépendance directe se choisit ; une dépendance transitive arrive avec elle, décidée par un mainteneur qu’on ne connaît pas, résolue par un algorithme qu’on n’a pas lu. Elles forment l’essentiel du code exécuté et des vulnérabilités signalées. La position défendue ici : tout verrouiller, tout inventorier, et mettre à jour par lots revus plutôt qu’au fil de l’eau.

→ Lire l’explication complète de la marmotte

Résolution des dépendances transitives : qui décide de la version

Section intitulée « Résolution des dépendances transitives : qui décide de la version »

Le fichier de déclaration exprime des contraintes ; le résolveur du gestionnaire de paquets choisit les versions réelles, et chaque écosystème tranche différemment :

  • npm accepte plusieurs versions du même paquet dans l’arbre et déduplique quand les plages se recoupent. Une plage ^1.2.0 accepte toute version 1.x ultérieure : sans fichier de verrouillage, l’installation du lendemain peut embarquer un code différent.
  • pip n’installe qu’une version par environnement : le résolveur cherche une combinaison compatible et peut rétrograder une dépendance pour satisfaire une autre.
  • Maven applique la médiation « nearest wins » : la version la plus proche de la racine l’emporte, même si elle est plus ancienne qu’une version exigée plus bas. Un correctif de sécurité tiré en profondeur peut ainsi disparaître silencieusement.
  • Go utilise la sélection de version minimale (MVS, Minimal Version Selection) : la version retenue est la plus haute parmi les minimums exigés, sans jamais aller chercher plus récent. Résultat déterministe, et go.sum fixe les condensés.

Conséquence d’architecture : le fichier de verrouillage (package-lock.json, poetry.lock) est l’artefact de sécurité, pas le fichier de déclaration. Une revue de code qui ignore le diff du verrou revoit la partie qui compte le moins.

L’exécution à l’installation. Les scripts preinstall et postinstall de npm s’exécutent pour chaque paquet de l’arbre, transitifs compris, avec les droits de l’utilisateur qui lance l’installation. Sur un agent d’intégration continue (CI, Continuous Integration), ce sont les jetons de publication et les secrets de déploiement qui sont à portée. Contre-mesure : ignore-scripts=true dans .npmrc, avec une liste explicite des paquets autorisés à compiler (pnpm bloque ces scripts par défaut depuis sa version 10).

La mise à jour silencieuse. Un compte de mainteneur compromis publie une version corrective piégée ; toute plage ouverte la récupère à la prochaine résolution. Contre-mesure : verrou versionné, installation en mode strict (npm ci, pip install --require-hashes), et délai de quarantaine avant adoption d’une nouvelle version (option minimumReleaseAge de Renovate). Une version malveillante est en général retirée du registre en quelques jours : attendre une semaine filtre beaucoup.

La confusion de nom. Un paquet interne résolu sur le registre public, interrogé en parallèle. Contre-mesure : portées réservées (@organisation/), registre miroir unique en amont, et refus de toute résolution hors de ce miroir.

Vingt ans de métier apprennent une constante : le paquet qui pose problème n’est jamais celui qu’on a choisi avec soin après trois réunions, c’est celui dont personne ne savait qu’il existait, à la profondeur six.

Le verrou et les condensés garantissent que le code installé est celui qui a été résolu la première fois. Ils ne disent rien de l’honnêteté de ce code : une version piégée verrouillée reste piégée, avec un condensé parfaitement valide. Les signatures Sigstore et attestations de construction prouvent qui a publié et depuis quel dépôt, pas ce que fait le code ; le débat de fond est traité dans les éditoriaux.

Le verrou a aussi un coût, et c’est le compromis central du sujet : on gagne la reproductibilité, on paie en fraîcheur. La recommandation assumée : verrouiller strictement, et regrouper les mises à jour dans un flux automatisé (Renovate ou Dependabot) qui ouvre une demande de fusion revue, jamais fusionnée seule. La fusion automatique ne se défend que si les tests couvrent les chemins d’exécution et si l’installation n’a accès à aucun secret.

Autre angle mort : l’outil d’analyse de composition logicielle (SCA, Software Composition Analysis) signale la présence d’un paquet vulnérable, pas son atteignabilité. Une CVE (Common Vulnerabilities and Exposures) dans une fonction jamais appelée pèse moins qu’une CVE moyenne sur le chemin d’une requête HTTP (HyperText Transfer Protocol). govulncheck fait cette analyse d’appel pour Go ; ailleurs, le tri reste manuel.

Exemple reproductible : mesurer l’arbre et forcer une version transitive

Section intitulée « Exemple reproductible : mesurer l’arbre et forcer une version transitive »

Le bloc suivant utilise npm 10 et Node.js 22, dans un répertoire jetable.

Fenêtre de terminal
# Projet vierge, une seule dépendance déclarée, scripts d'installation désactivés
mkdir demo-transitif && cd demo-transitif
npm init -y
npm install --save-exact --ignore-scripts [email protected]
# Nombre de paquets réellement installés (première ligne = le projet lui-même)
npm ls --all --parseable | wc -l
# Pourquoi qs est présent : chaîne complète des dépendants
npm explain qs
# Vulnérabilités connues sur les dépendances de production
npm audit --omit=dev
# Forcer une version de qs partout dans l'arbre, même si express exige 6.11.0
npm pkg set overrides.qs=6.16.0
npm install --ignore-scripts
npm ls qs # doit afficher "[email protected] overridden"
# En CI : installer exactement le verrou, sans le modifier ni exécuter de script
npm ci --ignore-scripts

Une seule dépendance déclarée produit près de soixante-dix paquets installés. L’override corrige une vulnérabilité sans attendre que chaque maillon intermédiaire publie une version, mais il sort la bibliothèque de la plage testée par son dépendant : il se justifie par un avis de sécurité, se documente, et se retire dès que l’amont a rattrapé. Un overrides truffé d’entrées vieilles de deux ans est une dette, pas une politique.

Prérequis. Un registre miroir interne (Artifactory, Nexus, Verdaccio) par lequel passe toute résolution, un verrou versionné par projet, et une CI capable de refuser un verrou désynchronisé du fichier de déclaration.

Inventaire. Générer une nomenclature logicielle (SBOM, Software Bill of Materials) au format CycloneDX ou SPDX (Software Package Data Exchange) à chaque construction, et la stocker à côté de l’artefact livré. Elle répond, le jour où l’avis tombe, à la question « ce binaire contient-il X en version Y ? ». La page Développement situe cette brique dans la chaîne de construction.

Supervision. Une analyse à la construction ne voit que les CVE déjà publiées. Les SBOM des versions en production se réanalysent chaque jour contre une base comme OSV (Open Source Vulnerabilities) ; sinon, un artefact livré il y a six mois reste réputé sain par défaut.

Secrets. Les jetons d’accès au registre et de publication vivent dans le gestionnaire de secrets de la CI, à portée minimale et rotation régulière ; jamais dans un .npmrc versionné.

Reprise. Un paquet retiré du registre public casse toute construction qui le résout. Le miroir interne, qui conserve les versions consommées, en fait un non-événement, et sert de point de blocage : retirer une version compromise du miroir l’élimine des constructions à venir.

Sans inventaire des dépendances transitives, la surface réelle du projet reste inconnue : un paquet installé par un autre paquet n’apparaît dans aucun fichier de déclaration, donc dans aucune revue. La conséquence immédiate est une perte de détection : la publication d’un avis de sécurité sur une bibliothèque de troisième rang ne déclenche rien, puisque rien ne relie cet identifiant au code déployé. S’y ajoute une perte de traçabilité : en l’absence de verrouillage des versions résolues, deux constructions successives du même commit peuvent embarquer des arbres différents, ce qui rend l’analyse d’un incident invérifiable et la reprise incertaine. Enfin, un mainteneur secondaire compromis obtient un chemin d’exécution direct, au même niveau de privilège que le code applicatif, sans franchir la moindre étape de validation.

Un programme moderne n’est presque jamais écrit en entier par l’équipe qui le livre. Il réutilise des bibliothèques, c’est-à-dire des morceaux de code publiés par d’autres. Chacune de ces bibliothèques en utilise d’autres, qui en utilisent encore d’autres : ce sont les dépendances transitives. Au moment de l’installation, un outil appelé gestionnaire de paquets lit les versions demandées, calcule la liste complète, télécharge tout et, parfois, exécute des petits programmes fournis par ces paquets. Si une seule de ces pièces est piégée, elle tourne avec les mêmes droits que le reste du programme. Pour limiter le risque, on enregistre dans un fichier de verrouillage la version exacte et l’empreinte de chaque pièce : l’installation suivante reprend exactement la même liste, et une pièce modifiée est refusée. Ce fichier empêche le changement discret, mais il ne dit pas si le code est honnête, et il bloque aussi l’arrivée des corrections. Il faut donc mettre à jour régulièrement, en relisant les changements. On garde enfin la liste complète des pièces de chaque version livrée, et on la compare chaque jour aux failles nouvellement publiées : sans cette liste, une faille annoncée dans une pièce profonde passe inaperçue.

À lire aussi

Pages voisines