Le 22 octobre, Composer a publié sa 2.11.0-RC1, et la ligne la plus utile de son changelog tient en une phrase : une politique cooldown qui retient les versions de paquets publiées depuis moins d’une durée choisie. L’idée est simple : une version piégée, publiée depuis un compte de mainteneur compromis, est le plus souvent repérée et retirée en quelques heures ou quelques jours. Si votre projet refuse tout ce qui a moins d’une semaine, il ne la verra jamais passer. Je pense que c’est la protection de chaîne d’approvisionnement la moins chère qu’un projet PHP puisse s’offrir, y compris une application qui tourne encore en PHP 7.4 : Composer 2.11 demande toujours PHP 7.2.5 au minimum. Encore faut-il comprendre trois choses avant de l’activer : quelle date elle regarde, à quel moment elle agit, et comment faire passer un correctif de sécurité urgent.
Une ligne de configuration, et ce qu’elle laisse passer
La documentation de la 2.11 place la quarantaine dans le bloc policy déjà utilisé pour les avis de sécurité, les paquets abandonnés et les paquets signalés comme malveillants. Par défaut, la durée vaut null : rien ne change tant que vous ne la fixez pas.
{
"config": {
"policy": {
"cooldown": {
"period": "7 days",
"ignore": {
"mon-entreprise/*": "Nos paquets internes, relus avant publication"
}
}
}
}
}
La durée s’écrit "7 days", "24 hours", "1 week" ou en secondes ; les formules relatives comme "next week" sont refusées. Pendant un composer update ou un require, une version trop récente est simplement écartée, et le solveur prend la précédente. composer outdated et show --latest affichent la dernière version autorisée et signalent celle qui attend encore.
Quatre cas ne sont jamais retenus : les branches (dev-main), les plateformes (PHP et ses extensions), les paquets verrouillés que la commande ne met pas à jour, et les versions sans aucune date connue, que Composer laisse passer plutôt que de les bloquer pour toujours. Ce dernier point concerne surtout les dépôts package déclarés à la main dans un vieux composer.json, un motif que l’on croise souvent dans les projets qui ont dix ans.
La date qui compte est celle de Packagist, pas celle du tag
Le détail qui fait la valeur de la fonctionnalité se lit dans la discussion de la PR #12692. Les premières versions s’appuyaient sur le champ time du paquet, que l’auteur contrôle. Un attaquant qui publie une version piégée peut donc l’antidater. La version fusionnée lit un champ published-time, posé par le dépôt au moment où il voit la version, et que Packagist fournit.
Les deux dates diffèrent réellement, et pas de quelques secondes. Dans les métadonnées publiques de Packagist, symfony/polyfill-intl-normalizer 1.43.0 porte un time du 23 septembre 2026 et un published-time du 30 septembre. symfony/console 7.4.21 est daté du 12 octobre côté auteur et du 14 octobre côté Packagist. Une règle bâtie sur time aurait jugé ces versions plus vieilles qu’elles ne l’étaient pour votre projet.
Le revers concerne les dépôts privés. Un dépôt Composer qui ne fournit pas published-time fait échouer la commande dès que la quarantaine est active, en nommant le paquet fautif. C’est le cas d’un Satis, cité par la documentation. On peut alors poser require-published-time: false sur ce dépôt pour revenir au champ time, mais la documentation le dit sans détour : la protection est plus faible. Pour des paquets internes, une entrée ignore motivée est plus honnête qu’une date que personne ne contrôle.
Elle agit à l’update, jamais à l’install
C’est le point le plus facile à mal comprendre. La quarantaine s’applique quand le solveur choisit des versions : update, require, init, create-project. Un composer install lit le composer.lock et installe ce qui y est écrit, quel que soit l’âge des versions.
Dans une chaîne classique, la CI et la production font des install. Le moment dangereux est donc celui où quelqu’un régénère le lock : un développeur sur son poste, Dependabot ou Renovate dans une PR, un script de mise à jour planifié. C’est là que la règle doit être active, et c’est pourquoi elle doit vivre dans le composer.json versionné, pas dans une variable d’environnement de la CI. Une variable COMPOSER_POLICY_COOLDOWN_PERIOD existe, et elle sert très bien à forcer une durée sur un runner, mais elle ne protège pas le poste qui lance l’update.
Si vous utilisez déjà Dependabot, sa propre option cooldown couvre en partie le même besoin, avec 3 jours par défaut d’après la documentation de GitHub, et Composer figure dans la liste des écosystèmes. Elle ne concerne que les mises à jour de version, pas les mises à jour de sécurité, et elle ne s’applique qu’aux PR du bot. Les deux se complètent : le bot ne propose pas la version trop récente, et Composer la refuse si un humain la demande à la main.
Un piège pratique pour le jour où vous l’activez : la 2.11 considère désormais qu’un changement du bloc policy rend le lock obsolète (#13149). Ajouter la quarantaine fera donc apparaître l’avertissement habituel sur le content-hash. Faites le changement de configuration dans un commit à part, avec un composer update --lock, pour que la revue ne mélange pas règle et versions.
Le correctif de sécurité coincé en quarantaine
Le meilleur contre-argument est évident : si une faille sort aujourd’hui et que le correctif a été publié ce matin, une quarantaine de sept jours vous laisse exposé une semaine. La discussion de la PR montre que le cas a été pris au sérieux. Une première version laissait passer automatiquement les versions publiées après un avis de sécurité. Elle a été retirée, pour une raison que je trouve juste : les métadonnées d’avis sont partielles, et quiconque peut publier une version peut aussi déposer un avis. Une exemption automatique aurait offert à l’attaquant la porte de sortie.
Il n’y a donc pas de contournement automatique. Quand un avis bloque toutes les versions disponibles hors quarantaine, Composer liste chaque version avec sa raison et propose les sorties : une entrée ignore, ou un passage ponctuel.
# Faire entrer un correctif publié ce matin, pour cette commande seulement
composer update symfony/http-kernel --no-cooldown
# Ou fixer une durée plus courte le temps d'une intervention
COMPOSER_POLICY_COOLDOWN_PERIOD="24 hours" composer update symfony/http-kernel
--no-cooldown ne coupe que la quarantaine : les avis de sécurité et les listes de paquets malveillants continuent de s’appliquer, contrairement à --no-blocking. C’est cette commande-là qu’il faut écrire dans la procédure d’urgence, pour que personne ne prenne l’option la plus large par réflexe.
Mesurer l’exposition sans attendre la version finale
La 2.11 est en release candidate, et je ne mettrais pas une RC sur une chaîne de production. Rien n’empêche en revanche de mesurer dès aujourd’hui ce que la règle aurait retenu. Ce script lit un composer.lock, interroge les métadonnées publiques de Packagist et signale les versions verrouillées publiées depuis moins de N jours, à partir du même champ published-time.
<?php
// Usage : php cooldown-check.php composer.lock 7
[$_, $lockFile, $days] = $argv + [1 => 'composer.lock', 2 => '7'];
$limit = new DateTimeImmutable("-{$days} days");
$lock = json_decode(file_get_contents($lockFile), true, flags: JSON_THROW_ON_ERROR);
$exit = 0;
foreach ([...$lock['packages'], ...$lock['packages-dev'] ?? []] as $pkg) {
if (str_starts_with($pkg['version'], 'dev-')) {
continue; // une branche n'a pas de date de publication
}
$url = "https://repo.packagist.org/p2/{$pkg['name']}.json";
$meta = json_decode(@file_get_contents($url) ?: 'null', true);
$published = null;
foreach ($meta['packages'][$pkg['name']] ?? [] as $release) {
if (($release['version'] ?? null) === $pkg['version']) {
$published = $release['published-time'] ?? null;
break;
}
}
if ($published === null) {
echo "? {$pkg['name']} {$pkg['version']} : date de publication introuvable\n";
continue;
}
if (new DateTimeImmutable($published) > $limit) {
echo "!! {$pkg['name']} {$pkg['version']} publié le {$published}\n";
$exit = 1;
}
}
exit($exit);
Je l’ai exécuté sous PHP 8.3 sur un projet de test (symfony/console 7.4 et monolog/monolog 3, verrouillés le 28 octobre) : avec un seuil de 7 jours, rien ne sort ; avec 30 jours, six des onze paquets sont signalés, dont symfony/console et symfony/string publiés le 14 octobre. C’est un ordre de grandeur parlant à montrer en réunion : sur ce petit projet à jour, plus de la moitié des dépendances avait moins d’un mois. Lancé dans la CI sur les PR qui touchent le lock, avec un seuil de 2 ou 3 jours, il sert aussi de filet tant que l’équipe n’est pas passée en 2.11.
Le même projet de test m’a donné un deuxième résultat, moins rassurant. Avec Composer 2.8.12, la configuration policy.cooldown ci-dessus passe composer validate sans erreur et composer update --dry-run l’ignore sans un mot. Une équipe dont un poste ou un runner est resté sur une ancienne version croira donc être protégée. Fixez la version de Composer dans l’image de CI et vérifiez-la dans un job, plutôt que de compter sur la mémoire de chacun.
Les limites
La quarantaine protège contre un scénario précis : la version piégée publiée puis retirée rapidement. Elle ne fait rien contre une compromission lente, un paquet malveillant depuis sa première version, ou un mainteneur légitime qui casse quelque chose. Elle ne remplace ni la relecture des diffs de dépendances ni composer audit.
Elle suppose aussi que d’autres essuient les plâtres. Si tout l’écosystème attend sept jours, plus personne n’installe les versions fraîches et la détection rapide repose sur les outils automatiques des registres et des éditeurs de sécurité. C’est un équilibre collectif, pas une garantie individuelle.
Enfin, le choix de la durée est un compromis. Sept jours, l’exemple de la documentation, me paraît raisonnable pour une application métier ; une équipe qui livre plusieurs fois par jour et dépend de correctifs rapides préférera deux ou trois jours, avec la procédure --no-cooldown bien rodée. Cet article s’appuie sur la documentation et le changelog de la 2.11.0-RC1 ; je n’ai pas exécuté la RC elle-même, seulement le script ci-dessus et Composer 2.8.12.
Concrètement, par où commencer
- Lancez le script sur le
composer.lockde vos projets principaux avec 7 puis 30 jours, pour savoir quelle part de vos dépendances une quarantaine aurait retenue cette semaine. - Listez vos dépôts Composer privés et vérifiez s’ils fournissent
published-time; sinon, prévoyez une entréeignoremotivée pour vos paquets internes. - Fixez la version de Composer dans vos images de CI et sur les postes, et testez la 2.11 avec
composer self-update --previewsur un projet non critique. - Écrivez dès maintenant la procédure d’urgence pour un correctif de sécurité :
--no-cooldown, ciblé sur un paquet, jamais--no-blocking. - À la sortie de la version finale, ajoutez
policy.cooldowndans un commit dédié, aveccomposer update --lock.
La question suivante viendra vite : que valent nos dépôts privés et nos paquets forkés à la main, ceux qui n’ont ni date fiable ni avis de sécurité.
Sources
- Composer CHANGELOG, 2.11.0-RC1, GitHub, 22 octobre 2026
- Composer 2.11.0-RC1, GitHub, 22 octobre 2026
- Composer documentation, Config : policy et cooldown, GitHub, version 2.11.0-RC1
- Composer documentation, Repositories : require-published-time, GitHub, version 2.11.0-RC1
- Add cooldown policy config to filter newly released packages (PR #12692), GitHub, fusionnée le 1er octobre 2026
- Métadonnées Packagist de symfony/console, Packagist, consultées le 28 octobre 2026
- Dependabot options reference, GitHub Docs
Sur ce sujet, l’offre de l’atelier : Audit gratuit