Pourquoi les gestionnaires de paquets ignorent-ils les versions corrigées des paquets ?

Les correctifs php2shell (CVE-2026-109618 et CVE-2026-109619) pour les versions de PHP en fin de vie sont désormais disponibles. Les réponses du service commercial étant plus lentes que d'habitude en raison d'une forte demande, le moyen le plus rapide d'obtenir les paquets corrigés est d'acheter ELS for Runtimes en ligne (s'ouvre dans un nouvel onglet). Consulter la FAQ sur les correctifs

Accédez directement au résumé généré par l'IA de cet article de blog

  • Résumez cet article avec Perplexity
  • Résume cet article avec Gemini

Principaux enseignements

  • Les versions modifiées sont classées avant la version d'origine dans Maven et Python, mais après celle-ci dans npm et Gradle.
  • Sur les huit vérifications de contraintes effectuées dans quatre gestionnaires de paquets, une version corrigée n'a été retenue que trois fois.
  • La visibilité du résolveur, c'est-à-dire le fait qu'un résolveur choisisse ou non spontanément une version corrigée, détermine si un correctif sera finalement déployé.
  • La méthode qui fonctionne dans tous les cas consiste à fixer explicitement la version corrigée et à recompiler ce qui est livré.

Voici un scénario que nous avons vu se reproduire à plusieurs reprises. Une équipe applique rétroactivement un correctif de sécurité à une bibliothèque qu’elle ne peut pas mettre à jour, publie la version corrigée dans son registre interne, puis passe à autre chose. L’intégration continue (CI) affiche « vert ». Une semaine plus tard, le scanner signale toujours la vulnérabilité CVE, et lorsque quelqu’un ouvre enfin le fichier de verrouillage, la version corrigée n’y figure pas. Aucune erreur ne s’est produite. Le gestionnaire de paquets a simplement examiné la version corrigée, a jugé qu’elle ne répondait pas aux critères requis et a installé à la place la version vulnérable.

Ce n'est pas un bug dans npm, ou Gradle, ou tout autre outil. Chaque écosystème se contente de suivre à la lettre les règles de versionnement qui lui sont propres. Le problème, c’est que ces règles ont été rédigées pour les responsables de maintenance en amont qui publient des versions, et non pour ceux qui publient un correctif venant s’ajouter à la version d’un tiers. La distribution de paquets open source corrigés à travers les différents écosystèmes constitue une part importante de notre activité chez SecureChain, et la leçon la plus utile que nous en avons tirée est que le numéro de version que vous attribuez à un correctif a autant d’importance que le correctif lui-même.

Que signifie la « visibilité du résolveur » ?

La visibilité du résolveur désigne la capacité d'un gestionnaire de paquets à sélectionner de lui-même une version corrigée, compte tenu des contraintes de version déjà définies par un projet. Une version présentant une visibilité élevée du résolveur est prise en charge par les plages et les ancrages existants sans que personne n'ait à modifier le manifeste. Une version présentant une faible visibilité du résolveur reste dans le registre, pleinement disponible mais totalement ignorée, jusqu'à ce que quelqu'un l'intègre manuellement.

La visibilité dépend de deux facteurs. Le premier est l’ordre : il s’agit de savoir si l’écosystème considère que votre version corrigée est plus récente ou plus ancienne que la version en amont sur laquelle elle repose. Le second est la compatibilité : il s’agit de savoir si des contraintes courantes, telles qu’une plage de caractères ou une correspondance exacte, permettent de considérer la version corrigée comme acceptable. Ces deux questions appellent des réponses différentes selon les écosystèmes, ce qui explique pourquoi une stratégie qui fonctionne à merveille dans un contexte peut échouer discrètement dans un autre.

Une seule version, quatre verdicts différents

Pour illustrer cela, nous avons pris un paquet hypothétique donné, dans sa version en amont 4.7.3, ainsi qu'une version corrigée de cette même version, et nous avons testé les deux en appliquant la logique de version réelle utilisée par chaque écosystème. Cela signifiait que npm« le sien » semver la bibliothèque (7.6.0), le comparateur de versions de Maven 3.9.11, Gradle 8.14 effectuant la résolution à partir d'un référentiel local, et Python’s packaging bibliothèque, c'est-à-dire pip sert à évaluer les versions. La version corrigée s'écrivait 4.7.3-patched.2 partout sauf en Python, où c'était 4.7.3+patched.2, puisque c'est le format prévu par la spécification Python pour cette tâche. Chaque exécution correspondait à une nouvelle résolution, sans fichier de verrouillage pour faire obstacle.

Nous avons ensuite posé deux questions à chacun d'entre eux. En partant d'une fourchette de compatibilité normale (^4.7.0 pour npm, [4.7,4.8) pour Maven et Gradle, ~=4.7.0 pour Python), quelle version l'emporte ? Et avec 4.7.3 Si les deux sont déclarées comme versions exactes, laquelle l'emporte alors ?

Écosystème Classement des versions corrigées Sélection de produits compatibles Sélections Exact 4.7.3
npm Ci-dessous 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Maven Au-dessus de la version 4.7.3 Version corrigée Upstream 4.7.3
Gradle Ci-dessous 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Python (pip) Au-dessus de la version 4.7.3 Version corrigée Version corrigée

Sur huit cas, trois ont abouti à un résultat positif, dont deux concernaient Python. Maven et Gradle ont lu les mêmes fichiers POM à partir des mêmes référentiels, mais sont néanmoins parvenus à des conclusions opposées quant à la version la plus récente. Si cela vous surprend, vous n’êtes pas le seul. Cela a également surpris certaines personnes de notre équipe.

Pourquoi npm considère votre correctif de sécurité comme une version bêta

npm respecte les règles de gestion des versions sémantiques (SemVer), et selon SemVer, un tiret après le numéro de correctif marque le début d'une préversion. Les préversions sont classées après la version qu'elles précèdent, donc 4.7.3-patched.2 est considéré comme plus ancien que 4.7.3. En ce qui concerne npm En ce qui concerne ce point, votre version renforcée est une version candidate à la publication de la version vulnérable.

La mise en correspondance ne fait qu'empirer les choses. Par défaut, npm La fonction « range matching » ignore complètement les versions préliminaires, donc ^4.7.0 et ~4.7.0 ne sélectionnera jamais la version corrigée. Même une plage écrite sous la forme ^4.7.3-patched.0, qui a explicitement choisi de participer aux préversions, continue de préférer le format simple 4.7.3 lorsque les deux sont disponibles, car 4.7.3 est « plus récent ».

Le cas qui pose réellement problème aux équipes est plus discret que cela. Imaginons que votre registre interne contienne la version corrigée 4.7.3 une construction et un vieux 4.7.2, mais pas en amont 4.7.3. On pourrait s'attendre à ce que ^4.7.0 pour atterrir sur la zone. Lors de notre test, cela a donné lieu à 4.7.2, la version plus ancienne et toujours vulnérable, car 4.7.2 était la version non préliminaire la plus récente disponible. La correction était juste là et npm l'a enjambé.

Donc, dans npm, la version corrigée doit être nommée explicitement. Si foo s'il s'agit d'une dépendance directe, cela signifie simplement qu'il faut indiquer la version exacte dans dependencies. Pour les copies situées plus bas dans l'arborescence, npm les remplacements constituent l'outil, à définir dans le répertoire racine package.json de l'application :

{
  "overrides": {
    "foo": "4.7.3-patched.2"
  }
}

Cela oblige chaque copie de foo n'importe où dans l'arborescence de la version corrigée, que ce soit de manière transitive ou non, et npm ls foo vous permettra de savoir s'il s'est accroché. Il est utile de connaître quelques astuces. npm ne prend en compte que les redéfinitions présentes dans le projet racine ; une bibliothèque ne peut donc pas les définir pour ses utilisateurs. Si foo est également une dépendance directe ; son entrée dans dependencies doit correspondre exactement à la redéfinition (le "$foo" le formulaire de référence permet de synchroniser les deux), ou npm refuse de s'installer avec un EOVERRIDE erreur. Et le vaisseau de remplacement reste stationné sur l'ancienne base. Une fois que les vaisseaux en amont 4.7.4, tout projet ne comportant pas cette redéfinition y basculera sans problème via une plage de sélection, et 4.7.4 votre portage en amont ne sera pas disponible si le correctif n'a jamais été accepté en amont. C'est pourquoi nous considérons chaque publication d'un correctif en amont comme un signal pour recompiler le correctif sur la nouvelle base, de sorte que 4.7.4-patched.1 existe avant même que quiconque en ait besoin. Yarn’s resolutions et pnpm’s overrides suivent le même principe, mais avec leur propre syntaxe.

Maven et Gradle lisent le même fichier POM, mais leurs interprétations divergent

Le comparateur de versions de Maven reconnaît une liste restreinte de qualificatifs tels que alpha, beta, rc, et snapshot, et tout ce qu'il ne reconnaît pas est classé après la version standard. Ainsi, dans Maven, 4.7.3-patched.2 est plus récent que 4.7.3, et une gamme telle que [4.7,4.8) le choisit. C'est surtout une bonne nouvelle.

Le problème, c'est que presque personne n'utilise de plages dans Maven. Une version simple dans un fichier POM est une contrainte souple, et Maven résout les conflits en fonction de la distance par rapport à la racine de l'arborescence, et non en fonction de la version la plus récente. Ainsi, si la déclaration la plus proche indique 4.7.3, on obtient 4.7.3, point. L'ordre de traitement de Maven est compatible avec les versions modifiées, mais sa résolution ne se pose généralement même pas la question de l'ordre. Pour intégrer la version modifiée, il suffit de la déclarer dans dependencyManagement au niveau supérieur, ce qui prévaut sur tout ce que l'arbre transitif a demandé.

Gradle adopte une approche inverse en matière d'ordre. Selon sa règle, l'ajout d'un élément non numérique fait baisser le numéro de version, et patched ne fait pas partie de la poignée de mots particuliers (rc, snapshot, release, sp, et quelques autres) qui ont leur propre place dans la gamme. Donc 4.7.3-patched.2 tri ci-dessous 4.7.3. Gradle résout également les conflits en choisissant la version la plus récente dans le graphe ; ainsi, lorsqu'une dépendance exige la version corrigée et qu'une autre exige 4.7.3, notre version de test a été intégrée au code source en amont 4.7.3. La version vulnérable a été retenue car Gradle l'a considérée comme plus récente. La correction est une version stricte, déclarée avec strictly, soit sur la dépendance elle-même, soit à l'intérieur d'une contrainte de dépendance, comme dans version { strictly '4.7.3-patched.2' }. Une simple contrainte ne suffit pas ici, car Gradle la considère uniquement comme une valeur minimale et 4.7.3 est déjà considérée comme prioritaire par rapport à la version corrigée aux yeux de Gradle.

Python fonctionne correctement, à condition de choisir le bon séparateur

La spécification de version de Python, la PEP 440, comporte un élément que les autres ne possèdent pas : un indicateur de version local spécialement conçu pour ce cas de figure. Tout ce qui suit le signe « + » est un indicateur local, et 4.7.3+patched.2 catégories ci-dessus 4.7.3. Mieux encore, une épingle publique comme ==4.7.3 ignore le libellé local lors de la mise en correspondance ; ainsi, un projet déjà associé à 4.7.3 acceptera 4.7.3+patched.2 et je préfère cette option. Aucune modification du manifeste n’est nécessaire. C’est la seule ligne du tableau où les deux réponses renvoient la version corrigée. Les fichiers de verrouillage ont toutefois toujours leur mot à dire. Un fichier de verrouillage ou un fichier de dépendances avec hachage fixe conserve l’ancienne version jusqu’à ce que vous la régénériez, et un environnement qui dispose déjà de 4.7.3 Une fois installé, il reste en place jusqu’à ce que quelqu’un le fasse fonctionner pip install --upgrade.

Le piège consiste plutôt à utiliser une publication a posteriori. 4.7.3.post1 est encore plus élevé, mais ==4.7.3 ne correspond pas à celle-ci ; par conséquent, tous les projets liés à la version en amont exacte l'ignorent sans avertissement. Si vous publiez des versions de Python modifiées, c'est l'étiquette locale qui est transmise aux projets liés. C'est en utilisant une version « post-release » que vous obtenez un correctif qui ne concerne que les utilisateurs qui étaient déjà en train de mettre à jour leur version.

Il y a toutefois un petit problème pratique. PyPI n'accepte pas les fichiers comportant des étiquettes de version locales ; les versions corrigées doivent donc être hébergées ailleurs, généralement sur un répertoire privé. La manière dont ce répertoire est configuré a également son importance en matière de sécurité. pip ne privilégie pas un index par rapport à un autre. Il se contente de sélectionner le résultat le plus pertinent parmi toutes les sources qu’on lui demande d’explorer. Ainsi, en indiquant pip Il est plus sûr d'utiliser un seul index qui gère à la fois vos builds et vos paquets en amont plutôt que d'ajouter un index supplémentaire, ce qui risquerait d'entraîner une confusion au niveau des dépendances.

Ce que cela implique si vous distribuez ou utilisez des paquets corrigés

La leçon que nous avons tirée de tout cela, c’est qu’on ne peut compter sur aucun résolveur pour trouver une solution tout seul. Python s’en rapproche le plus, mais même dans ce cas, tout dépend du choix de la bonne syntaxe de version. Partout ailleurs, la version corrigée doit être explicitement fixée, qu’il s’agisse d’une redéfinition, d’un dependencyManagement entrée, ou une version stricte, et tout ce qui est déjà déployé doit être recompilé pour que la broche y figure bel et bien. Si vous êtes propriétaire de l’application, cela ne représente qu’une seule modification à un seul endroit. Si vous publiez des paquets utilisés par d’autres équipes et que le correctif se retrouve trois niveaux plus bas dans l’arborescence, cela implique de recompiler également les paquets situés au-dessus, dans l’ordre, jusqu’à ce que l’artefact de niveau supérieur intègre la version corrigée, car personne en aval ne va ajouter votre remplacement à votre place. Cela représente plus de travail que la publication d’un seul artefact, mais c’est la seule approche que nous ayons trouvée qui se comporte de la même manière dans les quatre écosystèmes.

L'autre leçon à retenir est de ne plus se fier à la chaîne de version pour déterminer le statut de sécurité. Un suffixe indique au résolveur à quelle catégorie appartient une version. Il ne fournit aucune information fiable sur les CVE que cette version corrige réellement, et de nombreux scanners vont considérer 4.7.3-patched.2, le faire correspondre à 4.7.3, et signaler chaque CVE connu pour cette version. C'est grâce à un enregistrement lisible par machine répertoriant les correctifs apportés — par exemple un document VEX joint à la version — qu'un scanner peut faire la distinction, à condition bien sûr qu'il le lise effectivement.

Tout cela n’a rien d’exotique. Il s’agit simplement de l’aspect de la sécurité de la chaîne d’approvisionnement qui réside au cœur des gestionnaires de paquets, et c’est là que beaucoup de bonnes corrections finissent par être abandonnées. Si vous gérez des branches de versions que les développeurs en amont ont cessé de prendre en charge – ce qui correspond exactement à ce que couvre le service « Endless Lifecycle Support » de TuxCare –, cela vaut la peine de consacrer un après-midi à vérifier ce que vos propres résolveurs installent réellement.

FAQ

Qu'est-ce que la visibilité du résolveur ?

La visibilité du résolveur désigne la capacité d'un gestionnaire de paquets à sélectionner de lui-même une version corrigée, compte tenu des contraintes de version déjà déclarées par un projet. Elle dépend de la manière dont l'écosystème classe la version corrigée par rapport à la version en amont, ainsi que de la correspondance avec les contraintes courantes telles que les plages de versions et les références exactes.

Pourquoi npm n'installe-t-il pas une version corrigée du paquet ?

npm traite toute version dont le suffixe comporte un trait d'union, comme 4.7.3-patched.2, en tant que préversion. Les préversions sont classées après la version correspondante et ne sont pas prises en compte par les sélections standard à l'aide du signe d'accent circonflexe et du tilde, donc npm installe à la place la version en amont ou une version antérieure.

Comment imposer une dépendance transitive modifiée dans npm ?

Ajouter un overrides accès à la racine package.json qui indique la version exacte de la mise à jour. npm puis utilise cette version pour chaque copie du paquet dans l'arborescence des dépendances, y compris les dépendances transitives. Si le paquet est également une dépendance directe, sa version dans dependencies doit correspondre à la redéfinition, ou npm se termine par un EOVERRIDE erreur. Les redéfinitions dans une bibliothèque package.json sont ignorés lorsque cette bibliothèque est installée en tant que dépendance.

Maven considère-t-il un qualificateur de version personnalisé comme plus récent que la version publiée ?

Oui. Maven classe les qualificatifs non reconnus après la version standard, donc 4.7.3-patched.2 est plus récent que 4.7.3. Cependant, Maven résout généralement les conflits en fonction de la distance dans l'arborescence des dépendances plutôt qu'en fonction de la version ; vous devez donc tout de même dependencyManagement pour vous assurer que la version corrigée est bien sélectionnée.

Pourquoi Gradle privilégie-t-il la version en amont plutôt qu'une version corrigée ?

Gradle traite une partie supplémentaire non numérique comme -patched.2 comme étant antérieure à la version standard, et son système de résolution des conflits retient la version la plus récente. Déclarez la version corrigée avec strictly, soit au niveau de la dépendance, soit à l'intérieur d'une contrainte de dépendance, pour l'imposer.

Est-ce que « pip install 4.7.3+patched.2 » fonctionnera avec une contrainte « ==4.7.3 » ?

Oui, à condition que la version corrigée figure dans un index pip comme on peut le voir. Conformément à la spécification PEP 440, un identifiant public ignore les étiquettes de version locales lors de la comparaison, et une version locale est classée avant la version publique ; ainsi, une nouvelle installation sélectionne la version corrigée. Un environnement qui dispose déjà de 4.7.3 besoins pip install --upgrade, et il faut d'abord régénérer un fichier de verrouillage.

Résumé
Pourquoi les gestionnaires de paquets ignorent-ils les versions corrigées des paquets ?
Nom de l'article
Pourquoi les gestionnaires de paquets ignorent-ils les versions corrigées des paquets ?
Description
Une version corrigée, quatre gestionnaires de paquets, et seuls 3 des 8 contrôles de contraintes l'ont détectée. Voici pourquoi, et comment s'assurer que la correction soit prise en compte.
Auteur
Nom de l'éditeur
de TuxCare
Logo de l'éditeur

Rejoignez plus de 4 500 professionnels de Linux et de l'Open Source!

2 fois par mois. Pas de spam.

Table des matières
Obtenez les réponses à vos questions sur la sécurité des logiciels libres

Rejoignez plus de 4 500 professionnels de Linux et de l'Open Source!

2 fois par mois. Pas de spam.