Principaux enseignements
- Packagist répertorie les balises Git au lieu d'héberger les artefacts ; ainsi, jusqu'en juillet 2026, une version publiée pourrait être discrètement redirigée vers un autre commit.
- Deux incidents survenus en 2026 ont utilisé la même technique : l'un consistait à enregistrer un plugin Composer malveillant, et l'autre à détourner l'autochargeur de PHP.
- Un engagé
composer.lockil fixe la version qui a déjà été résolue, de sorte qu’une réécriture ultérieure de la balise ne puisse pas modifier ce quecomposer installtiraillements. - Packagist refuse désormais les modifications de balises sur les versions stables déjà publiées, mais il faut encore vérifier les anciens fichiers de verrouillage et les branches de développement.
Les autres registres majeurs ont tous pris cette décision très tôt. Une fois qu'une version est publiée, son contenu est figé. Les archives npm sont immuables. PyPI ne vous permet pas de republier un fichier sous le même nom. crates.io autorise le retrait, mais pas la suppression. Maven Central ne supprime en aucun cas un artefact publié.
Packagist a adopté une approche différente. Il ne stocke pas les paquets. Il lit un dépôt Git, indexe les balises qu’il y trouve et indique à Composer d’où les récupérer. Une version telle que 2.4.1 correspond à une balise Git associée à la référence de commit enregistrée par Packagist ; jusqu’en juillet 2026, cette balise pourrait être supprimée puis republiée en référence à un autre commit, Packagist s’adaptant discrètement à ces changements.
C'est cette faille que les attaques de la chaîne d'approvisionnement « Composer 2026 » ont exploitée, et PHP en a été la victime à deux reprises, à trois semaines d'intervalle.
Pourquoi Packagist est un index plutôt qu'un registre
C'est la différence architecturale qui fait la différence en matière de sécurité. Lorsque Composer résout une dépendance, Packagist fournit des métadonnées : le nom du paquet, ses versions et l'emplacement de chaque version, y compris les références source et dist qui correspondent généralement à un hachage de commit. Le code proprement dit provient du dépôt source ou d'une archive de distribution que l'hébergeur du code génère à la demande à partir d'une référence spécifique. Aucun élément de cette chaîne n'est un artefact immuable stocké par Packagist lui-même.
| Registre | Identité de version | Une version déjà publiée peut-elle être modifiée ? |
|---|---|---|
| npm | Une archive tar publiée | Non, les versions sont immuables |
| PyPI | Un fichier téléchargé | Non, les fichiers téléchargés ne peuvent pas être remplacés |
| crates.io | Des archives publiées | Non, uniquement pour les Américains |
| Maven Central | Un ouvrage publié | Non, la suppression n'est pas autorisée |
| Packagist | Une balise Git et une référence de commit indexée | Ce n'est qu'à partir de juillet 2026 que les réétiquetages ont été mis en place ; avant cette date, ils étaient déjà pratiqués. |
Cette dernière ligne résume à elle seule tout ce qu’il faut savoir sur les risques liés à la chaîne d’approvisionnement de PHP, et il ne s’agit pas d’un bug introduit par accident par Packagist. L’indexation de Git permet de maintenir les frictions liées à la publication à un niveau exceptionnellement bas, ce qui constitue depuis longtemps l’un des avantages concrets de Packagist.
Retour à la réalité : qualifier cela de « vulnérabilité de Packagist » revient à passer à côté de la véritable source de cette mutabilité. Les balises Git ont toujours été, par conception, déplaçables. Packagist a hérité de cette propriété en choisissant d’indexer les dépôts plutôt que d’héberger les artefacts, et c’est précisément ce compromis qui rend la publication d’un paquet Composer relativement légère.
À quoi ressemble réellement une attaque de la chaîne d'approvisionnement d'un compositeur ?
Le 30 avril 2026, une personne disposant d'un compte de service piraté a poussé un commit malveillant vers le dépôt intercom/intercom-php et y a attribué la balise 5.0.2 existante, selon le Avis de GitHub. La version réécrite composer.json a déclaré le paquet comme plugin Composer, et une fois approuvé – soit via une invite interactive, soit via un processus permissif allow-plugins cadre – cela s’est déroulé pendant composer install sous la forme d'un dropper qui récupérait l'environnement d'exécution Bun et exécutait un programme de vol d'identifiants obfusqué. Le tag malveillant est resté actif pendant moins de deux heures avant que les responsables ne le redirigent vers un code sain. Les chercheurs l'ont baptisé « Mini Shai-Hulud » et l'ont associé à la campagne qui a touché Le client npm d'Intercom dans la même fenêtre.
Trois semaines plus tard, cette même faille a été exploitée à une échelle bien plus grande. À partir de 22 h 32 UTC le 22 mai et jusqu’au 23 mai, un attaquant disposant d’un accès « push » à l’échelle de l’organisation a réécrit toutes les balises existantes dans quatre paquets de localisation Laravel-Lang, en les redirigeant vers des commits d’une branche qu’il contrôlait, de sorte qu’aucun code malveillant n’ait jamais été intégré aux dépôts officiels. Socket a recensé plus de 700 versions historiques remappées, Aikido en a confirmé 233, et la principale vague de réécriture a duré moins de 90 minutes. Cette fois-ci, la charge utile a détourné l'autochargeur de PHP au lieu d'enregistrer un plugin.
Aucun de ces incidents n'a nécessité la publication d'une nouvelle version. Aucune entrée dans le journal des modifications, aucun nouveau numéro de version, rien qui aurait pu être signalé comme une différence par un responsable de maintenance ou un bot de dépendances surveillant les numéros de version.
Comparez cela à la manière dont un pirate doit s’y prendre sur npm. Remplacer une version légitime sur cette plateforme implique de publier un numéro de version différent, car les octets qui composent un paquet et une version existants ne peuvent pas être remplacés à votre insu. Une nouvelle version apparaît alors dans les flux de publication et déclenche les robots de gestion des dépendances. Le déplacement d’une balise n’entraînait aucun de ces effets.
| Phase d'attaque | npm | Packagist avant juillet 2026 |
|---|---|---|
| Publier du code malveillant | Un nouveau numéro de version est requis | La version existante pourrait faire l'objet d'un réacheminement |
| Événement de lancement | Oui | Non |
| Détecté par un outil de comparaison de versions | Oui | On l'oublie souvent, mais le numéro de version n'a jamais changé |
| Annulé par l'auteur du code source | La fenêtre « Retirer de la publication » s'affiche | La balise pourrait tout simplement être replacée à son emplacement initial |
Cette réversion avait des avantages et des inconvénients. La récupération était rapide — un responsable de maintenance pouvait simplement replacer la balise à son emplacement d’origine, ce qui est exactement ce qui s’est passé avec intercom-php cette même nuit — mais l’analyse rétrospective devenait plus difficile pour la même raison, car un numéro de version pouvait désormais pointer vers un commit différent de celui auquel il renvoyait auparavant. Le journal public de transparence de Packagist a enregistré ces changements de référence, ce qui permet au moins de conserver une trace durable du moment où une référence publiée a été déplacée.
Deux charges utiles différentes, une balise modifiable
Ces deux incidents survenus en 2026 étaient, au sens strict, des attaques visant la chaîne d'approvisionnement de Composer, et tous deux ont débuté par la même ruse. La suite des événements a toutefois été radicalement différente, et cette distinction revêt une grande importance lorsqu'il s'agit de déterminer les mesures correctives à mettre en œuvre.
La balise compromise sur intercom/intercom-php renvoyait vers un commit qui modifiait composer.json pour déclarer le paquet comme un plugin Composer. C’est la variante de cette attaque que la plupart des articles abordent, car elle s’intègre parfaitement au système de plugins de Composer. Un plugin autorisé s’exécute pendant composer install et composer update, avant que le code de votre application ne soit exécuté.
La faille Laravel-Lang n'a jamais affecté l'enregistrement des plugins. Elle a permis d'injecter un code malveillant src/helpers.php dans le autoload.files entrée, de sorte que la charge utile s'exécutait à chaque requête PHP dès l'installation du paquet. Même balise « moved », même confiance accordée par Composer à ce vers quoi résout cette balise, mais deux chemins d'exécution complètement différents. Une correction apportée à l'un n'a aucun effet sur l'autre.
Vous pouvez également découvrir les principales stratégies de protection du framework dans notre guide détaillé sur la sécurité de Laravel.
La sécurité du plugin Composer présente une véritable faille
Une balise déplacée injecte du code. Quelque chose doit l'exécuter, et dans le cas d'une attaque de type « intercom », ce « quelque chose » est le système de plugins. Un paquet déclarant "type": "composer-plugin" est chargé et peut exécuter du code dans le cadre des processus d’installation et de mise à jour de Composer dès qu’il y est autorisé. C’est le comportement prévu, car les plugins ont pour but de modifier la manière dont Composer procède à l’installation, ce qui implique qu’ils s’exécutent avant la fin de l’installation.
La version 2.2.0 de Composer a ajouté la allow-plugins config, et la configuration par défaut actuelle n'autorise absolument rien. Dans composer.json Ça ressemble à ça.
{
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"phpstan/extension-installer": true
}
}
}
Avec allow-plugins Une fois cette configuration définie, Composer refuse d’exécuter tout plugin ne figurant pas dans la liste. Un plugin non répertorié déclenche une invite lorsque Composer s’exécute en mode interactif ; en mode non interactif, Composer échoue au lieu de poursuivre l’opération en silence, à moins que le plugin ne se déclare lui-même facultatif. Mieux vaut un échec de compilation qu’une installation silencieuse.
Attention : Vérifiez si quelqu'un a défini "allow-plugins": true simplement pour faire disparaître un message d'alerte. Cette seule ligne autorise tous les plugins, actuels et futurs, et désactive complètement la protection par liste blanche. L'exécution de composer config allow-plugins indique les paramètres réellement configurés pour un projet, et false c'est la valeur qui nie tout.
Pour une compilation ne nécessitant aucun plugin, ces options sont simples et efficaces.
composer install --no-plugins --no-scripts --no-dev
Si votre version de production ne nécessite ni plugins ni scripts Composer, une solution raisonnable consiste à les désactiver dans cet environnement tout en les laissant disponibles sur les machines des développeurs.
Les fichiers à chargement automatique constituent la solution la plus discrète
allow-plugins Cela ne résout en rien le problème des attaques de type « Laravel-Lang ». autoload.files Ce n'est pas une fonctionnalité optionnelle que l'on peut subordonner à une demande d'autorisation. Il s'agit d'une pratique courante utilisée par les paquets PHP pour fournir des fonctions d'aide, et Composer doit se fier à la configuration d'autochargement de chaque paquet pour que l'écosystème puisse fonctionner.
C’est ce qui a rendu cette variante de l’attaque particulièrement dévastatrice. Composer vous demande si vous faites confiance à un paquet pour qu’il s’exécute en tant que plugin. Il ne vous demande jamais si vous faites confiance à un paquet pour définir des fichiers chargés automatiquement. Avant la mise en place de la politique relative aux logiciels malveillants de Composer 2.10 et le changement de politique de Packagist concernant l’immuabilité, la principale protection consistait à ne jamais résoudre la balise réécrite, ce qui expliquait l’importance cruciale du fichier de verrouillage.
La seule chose que Composer fait bien et que npm ne fait pas
Les scripts Composer déclarés par une dépendance ne s'exécutent pas. Seuls les scripts du paquet racine s'exécutent. Un script malveillant post-install-cmd Le fait qu'il se trouve à quatre niveaux de profondeur dans votre arborescence n'a absolument aucune incidence. C'est un comportement par défaut nettement meilleur que celui de npm, où n'importe quel paquet de l'arborescence peut inclure un script post-installation qui s'exécute pendant l'installation.
| Mécanisme | Exécutions pour les dépendances |
|---|---|
| Scripts du cycle de vie npm | Oui, n'importe quel paquet de l'arborescence |
Python .pth hooks de démarrage |
Oui, au démarrage de l'interpréteur |
Rouille build.rs |
Oui, lors de la compilation |
| Écrire des scripts | Non, uniquement le paquet racine |
| Plug-ins Composer | Oui, si cela est autorisé |
Le saviez-vous ? Le fait de ne pas prendre en compte les scripts de dépendances est un choix de conception délibéré de Composer, et non une mesure de sécurisation ajoutée a posteriori à la suite d'un incident. C'est l'un des détails les plus souvent négligés lorsqu'on compare concrètement ces deux écosystèmes.
Ce que votre fichier de verrouillage Composer enregistre et ce qu'il ne prend pas en compte
composer.lock est plus protecteur que ne le laisse supposer sa réputation, et cette protection a une limite bien précise, qui couvre justement à la fois les attaques par plugin et les attaques par chargement automatique, car elle opère à un niveau inférieur à ces deux types d'attaques.
Pour chaque paquet, le fichier de verrouillage enregistre la version résolue ainsi que les références source et dist, qui correspondent généralement au hachage du commit Git associé à cette version. Exécutez composer install et Composer demande précisément ce commit-là plutôt que ce vers quoi pointe la balise aujourd’hui ; ainsi, une réécriture ultérieure de la balise n’entre jamais en ligne de compte. La garantie porte sur le commit demandé, et non sur une somme de contrôle des octets renvoyés, car les hébergeurs de code génèrent ces archives à la demande et le fichier de verrouillage ne contient aucun hachage exploitable pour celles-ci. Les opérations de réévaluation des dépendances constituaient le point de vulnérabilité, ce qui inclut les mises à jour partielles telles que composer update foo -W. L'article de Packagist consacré à la sécurité aborde le même sujet concernant les fichiers de verrouillage, ce qu'il est bon de savoir car de nombreux articles sur Laravel-Lang affirmaient que ces fichiers n'offraient aucune protection.
| Fonctionnement | Conserve la référence verrouillée | Exposé à une balise déplacée |
|---|---|---|
composer install avec le fichier de verrouillage validé |
Oui | Non |
composer update |
Non, ça se résout à nouveau | Oui |
| Mise à jour partielle d'un paquet | Uniquement pour les entrées qu'il ne modifie pas | Oui, pour tout ce qu'il ré-évalue |
composer require newpackage |
En partie | Oui, pour la nouvelle exigence et toutes les dépendances associées |
| Clone récent avec le fichier de verrouillage ignoré par Git | Non | Oui |
composer install dans une image Docker sans que le fichier de verrouillage ait été copié |
Non | Oui |
Un fichier Dockerfile qui ne copie que composer.json Avant l'exécution de l'installation, il n'y a pas de fichier de verrouillage à partir duquel travailler, et la protection disparaît sans que personne n'ait touché à une seule ligne de code de l'application. Copier composer.lock et, par la même occasion, rétablit la protection.
git ls-files composer.lock
Si cela ne renvoie aucun résultat, composer.lock n'est pas suivi dans ce dépôt ; par conséquent, à chaque nouvelle compilation, les dépendances sont résolues à nouveau en fonction des métadonnées du registre telles qu'elles apparaissent à ce moment-là.
Détection des logiciels malveillants et modification relative à l'immuabilité de juillet
Les incidents survenus en mai n'ont pas resté sans suite. Nils Adermann et Igor Benko ont publié, le 27 mai, une feuille de route en matière de sécurité pour Composer et Packagist, et la version 2.10 de Composer a été mise à disposition dès le lendemain.
Un élément de cette version revêt ici une importance particulière. Packagist importait déjà les résultats de détection de logiciels malveillants d’Aikido depuis mars, et c’est la version 2.10 qui a permis d’en tirer pleinement parti dans la pratique. Les versions signalées sont exclues du pool de résolution, de sorte que composer update, composer require et composer create-project ne peut pas les joindre, et composer install génère une erreur très visible si une version marquée figure déjà dans votre fichier de verrouillage, au lieu de l'intégrer discrètement. composer audit indique les mêmes versions et échoue par défaut.
En juin, les organisations utilisant Private Packagist ont également bénéficié de contrôles de restriction des plugins, permettant à une organisation de déterminer à l’avance quels paquets peuvent servir de plugins Composer. Lorsque cette fonctionnalité est activée, une version corrompue qui se déclare soudainement comme un plugin est supprimée des métadonnées de mise à jour et n’atteint jamais le résolveur de Composer ; toutefois, les installations à partir d’un fichier de verrouillage existant nécessitent également le paramètre distinct de blocage des téléchargements de Private Packagist.
La modification structurelle la plus importante a été mise en place le 7 juillet, lorsque Packagist a lancé les versions immuables. Une fois qu’une version stable est publiée, la référence Git qui lui est associée ne change plus. Que l’on supprime, recrée ou déplace la balise en amont, Packagist conserve la référence d’origine, affiche un badge indiquant que le re-tagging est bloqué sur la page du paquet et envoie un e-mail à chaque responsable de maintenance.
Les versions de développement liées à des branches continuent d'évoluer, puisque c'est là l'intérêt d'une branche, et la règle porte sur la correspondance entre la version et la validation plutôt que sur les octets de l'archive, car la plupart des fichiers ZIP sont encore générés à la demande par l'hébergeur du code. Les modifications de références bloquées, les suppressions temporaires et les restaurations sont toutes consignées dans le journal public de transparence.
Rien de tout cela ne permet de nettoyer rétroactivement un fichier de verrouillage qui a enregistré une modification malveillante avant la mise en place des protections, et c'est précisément pour cette raison que composer.lock et allow-plugins vous devez tout de même vérifier vos propres projets, quels que soient les résultats indiqués par le scanner.
Vérification des paquets avant même que le tag ne soit résolu
Dans les deux cas, la référence dangereuse a été intégrée au graphe de dépendances au moment de la résolution, bien avant le chargement d’un plugin ou le déclenchement d’un détournement par l’autochargeur. Composer 2.10 intègre désormais également une vérification anti-malware au moment de l’installation, même si cette vérification reste subordonnée au fait qu’une version ait d’abord été signalée par un utilisateur.
SecureChain pour PHP offre aux équipes une source de paquets recompilés à partir du code source et analysés pour détecter les logiciels malveillants, au lieu de recourir directement aux références du système de contrôle de version (VCS) en amont. Chaque paquet est recompilé dans un environnement isolé et vérifié avant d’être proposé à l’installation ; vous récupérez donc exactement ce qui a été validé. Les vulnérabilités sont corrigées en continu dans le cadre du SLA, et le support à cycle de vie illimité assure la continuité de cette couverture même lorsqu’un responsable de maintenance cesse de publier des mises à jour. Cela revêt une importance capitale pour les paquets abandonnés, pour lesquels la surveillance du responsable de maintenance, qui permettrait de détecter un changement inattendu en amont, a déjà pris fin.
Rien de tout cela ne remplace composer.lock, allow-plugins ou composer audit. Cela résout la partie du problème qui se situe en amont de tout ce que vos propres outils peuvent détecter. Voyez comment SecureChain pour PHP fonctionne.
FAQ
Qu'est-ce qu'une attaque de la chaîne d'approvisionnement de Composer ?
Une attaque qui compromet un paquet PHP avant même qu'il n'atteigne votre projet, généralement par le biais d'un dépôt compromis, d'une balise Git déplacée ou d'un plugin Composer malveillant, plutôt que par une vulnérabilité dans le code que vous avez écrit. Il s'agit d'une variante spécifique du modèle plus large d'attaque de la chaîne d'approvisionnement qui touche les registres open source dans tous les langages.
La version d'un paquet Packagist peut-elle changer après sa publication ?
Historiquement, c'était possible. Packagist indexait les balises Git plutôt que de stocker des artefacts immuables ; ainsi, une balise pouvait être supprimée puis ré-ajoutée à un autre commit sans que le numéro de version ne change. Depuis juillet 2026, Packagist rejette toute tentative de modification de la référence source d’une version stable déjà publiée ; il s’agit d’une protection côté serveur qui s’applique quelle que soit la version de Composer utilisée par le développeur. Les versions de développement suivant des branches évoluent toujours, et cette modification ne peut garantir la sécurité des installations existantes si celles-ci ont déjà résolu un commit malveillant par le passé.
Les scripts de dépendances de Composer s'exécutent-ils pendant l'installation ?
Non. Seuls les scripts du paquet racine s’exécutent. Un fichier malveillant post-install-cmd déclarée par une dépendance est ignorée, ce qui correspond à un comportement par défaut plus strict que celui de npm.
Comment les plugins Composer exécutent-ils le code ?
Un colis contenant "type": "composer-plugin" peut exécuter du code dans le cadre des opérations d’installation et de mise à jour de Composer, une fois qu’il a été installé et autorisé. À partir de la version 2.2.0 de Composer, les plugins doivent être explicitement ajoutés à la liste blanche via le fichier allow-plugins config.
Le fichier `composer.lock` offre-t-il une protection contre le déplacement d'une balise Git ?
Oui, si le fichier de verrouillage contient déjà la validation valide, car composer install utilise cette référence verrouillée au lieu de résoudre à nouveau la balise. Cela ne protège pas composer update, nouveau composer require appels ou compilations lorsque le fichier de verrouillage est absent.
À quoi sert le paramètre « allow-plugins » lorsqu'il est défini sur « true » ?
Cela autorise tous les plugins Composer, actuels et futurs, ce qui supprime complètement la protection. Remplacez-le par une liste explicite des plugins spécifiques dont votre projet a besoin.



