Blog / Symfony

Correctifs Symfony : la version citée par l’avis ne suffit pas

Un correctif Mime annoncé en mai jamais livré, des correctifs DoS en deux temps : comment savoir si votre composer.lock Symfony est vraiment à l’abri.

Par Romain Barberi Mis à jour le 5 novembre 2026 7 min de lecture 11 sources

Le 28 octobre, Symfony a publié 5.4.55, 6.4.49, 7.4.22 et 8.1.10. Le lendemain, un billet de Nicolas Grekas a expliqué pourquoi leurs notes citaient des failles déjà annoncées : le correctif de CVE-2026-45070, une injection d’en-têtes dans le composant Mime annoncée corrigée depuis le 20 mai, n’avait en réalité jamais été livré. Quinze jours plus tôt, deux des trois correctifs DoS du 15 octobre avaient déjà dû être repris. Ma position : pour une application Symfony qui vit depuis quelques années, « nous avons appliqué le correctif » n’est pas une réponse. La seule question utile est : quel outil me dit que mon composer.lock est à l’abri, et sur quelle base ?

Quatre CVE, deux livraisons ratées

Commençons par Mime. L’avis du 20 mai décrit un défaut de ParameterizedHeader : les valeurs de paramètres d’en-têtes structurés comme Content-Type ou Content-Disposition sont validées et encodées, mais pas leurs noms. Un nom dérivé d’une saisie qui contient \r\n termine l’en-tête courant et en injecte d’autres dans le message rendu. L’avis annonçait le correctif dans 5.4.52, 6.4.40, 7.4.12 et 8.0.12. C’était faux : le commit n’a jamais été fusionné dans une branche de maintenance, aucune release ne le contenait, et ce sont des utilisateurs qui l’ont signalé. Le vrai correctif est arrivé le 28 octobre.

Le 15 octobre, trois DoS ont été publiés le même jour. Celui de HttpFoundation (CVE-2026-108082) est le plus large : HeaderBag passe l’en-tête Cache-Control à HeaderUtils::split() à la construction de chaque Request, et une rafale d’espaces bien placée fait exploser le backtracking de l’expression régulière, sans authentification. Son premier correctif a introduit une régression. Celui d’ExpressionLanguage (CVE-2026-109449) était incomplet : une chaîne d’opérateurs binaires ou d’accès aux propriétés échappait encore à la limite de profondeur. Seul celui du lexer YAML (CVE-2026-109413) est passé du premier coup.

Rien de tout cela n’est un scandale. Les avis ont été corrigés publiquement, les raisons expliquées, et le billet du 29 octobre précise même que les trois DoS réapparaissent dans les notes de 6.4.49, 7.4.22 et 8.1.10 pour une raison purement mécanique : des commits de fusion sans code, refaits sur la 5.4 pour donner à chaque CVE le sien. Mais l’effet pour une équipe qui a lu les avis à leur sortie est le même : elle se croit protégée et ne l’est pas.

Les versions vraiment sûres, branche par branche

Voici la version minimale qui couvre chaque problème, d’après les avis Symfony dans leur état du 4 novembre.

Problème5.46.47.48.1
Mime, CVE-2026-450705.4.556.4.497.4.228.1.10
HttpFoundation, CVE-2026-108082, régression comprise5.4.546.4.487.4.218.1.9
Yaml, CVE-2026-1094135.4.546.4.457.4.188.1.6
ExpressionLanguage, CVE-2026-109449, correctif complet5.4.546.4.487.4.218.1.9
Yaml, GHSA-hwfg-qwvh-2wfx, et sept autres avis du 4 novembre5.4.566.4.507.4.238.1.11

La lecture est simple : sur chaque branche maintenue, seule la dernière version couvre tout. La 8.0 n’est plus maintenue ; pour elle, le chemin passe par 8.1.11, avec ce que ça implique de dépréciations à traiter. Sur les montées de version Symfony que j’ai menées, c’est exactement le cas que l’on repousse : une branche sortie du support qui « marche », jusqu’au jour où un correctif n’y arrivera plus.

Vérifier en une commande, et savoir ce qu’elle ne voit pas

J’ai reconstitué le composer.lock d’une équipe qui aurait appliqué chaque correctif dès sa première annonce : symfony/mime 7.4.19 (au-dessus de la 7.4.12 citée en mai), symfony/http-foundation 7.4.19, symfony/yaml et symfony/expression-language 7.4.18. Puis :

composer audit --locked --format=plain

Extrait du résultat, avec Composer 2.8.12 et PHP 8.3.6, le 30 octobre :

Found 2 security vulnerability advisories affecting 2 packages:
Package: symfony/expression-language
CVE: CVE-2026-109449
Affected versions: ... |>=7.4.0,<7.4.21|>=8.0.0,<8.1.0|>=8.1.0,<8.1.9
--------
Package: symfony/mime
Severity: medium
CVE: CVE-2026-45070
Affected versions: ... |>=7.4.0,<7.4.22|>=8.0.0,<8.1.0|>=8.1.0,<8.1.10
Reported at: 2026-10-28T18:08:54+00:00

La base que lit Composer est à jour : l’avis Mime porte la bonne plage depuis le soir du 28 octobre, et le correctif incomplet d’ExpressionLanguage est signalé. La commande sort avec un code non nul, ce qui suffit à faire échouer un job de CI.

En face, la fiche GitHub GHSA-vqc8-7275-q272 indiquait encore ce matin 7.4.12 comme version corrigée, avec une dernière mise à jour au 27 mai, alors que le billet du 29 octobre annonce sa correction. Or les alertes Dependabot s’appuient sur cette base, et seulement sur les avis relus par GitHub. Une équipe qui ne regarde que l’onglet sécurité de son dépôt voit donc, à l’heure où j’écris, un projet en 7.4.19 sans alerte sur Mime. Deux sources d’audit ne coûtent rien ; une seule peut vous laisser exposé plusieurs jours.

Reste le point aveugle des deux outils : la régression de HttpFoundation. Elle ne crée aucune faille, donc aucun avis ne la signale. Je l’ai reproduite en installant trois versions du composant :

use Symfony\Component\HttpFoundation\HeaderUtils;

var_export(HeaderUtils::split('foo bar', ' '));
// 7.4.18 : ['foo', 'bar']
// 7.4.19 : ['foo bar']
// 7.4.21 : ['foo', 'bar']

Ce script a été exécuté sur les trois versions, installées avec Composer dans un conteneur. Si votre code découpe un en-tête sur des espaces avec split(), la 7.4.19 lui a donné un résultat faux sans rien dire, et composer audit la considère comme saine.

Ce qui est exposé chez vous, faille par faille

Toutes ces CVE ne pèsent pas le même poids, et la priorité dépend de votre code.

  • HttpFoundation : toute application qui reçoit des requêtes HTTP. Aucun prérequis, c’est la plus urgente si vous êtes avant 7.4.19 (ou l’équivalent de votre branche).
  • Yaml : tout code qui parse du YAML venu de l’extérieur, et en particulier un contrôleur avec #[MapRequestPayload] quand le composant Yaml est installé et que l’endpoint ne restreint pas acceptFormat. Une requête avec un content type YAML suffit.
  • ExpressionLanguage : les applications qui valident des expressions saisies par l’utilisateur avec #[Assert\ExpressionSyntax], ou qui appellent lint() et parse() sur une entrée non fiable. Les règles métier configurables en back-office sont le cas typique d’un legacy.
  • Mime : les applications qui construisent des noms de paramètres d’en-tête à partir d’une saisie. C’est rare, mais un nom de pièce jointe ou un en-tête maison dans un outil d’envoi d’e-mails sont des candidats.

Pour les trois derniers, un grep donne une réponse en quelques minutes :

grep -rnE "MapRequestPayload|ExpressionSyntax|ExpressionLanguage|ParameterizedHeader|->addParameterizedHeader" src/

Il ne remplace pas la mise à jour, il dit seulement dans quel ordre traiter les applications quand vous en avez plusieurs.

Ce que cet article ne règle pas

Cet article s’appuie sur les avis et billets Symfony, la fiche GHSA et deux reproductions : composer audit sur un composer.lock reconstitué, et la régression de split() ; les failles elles-mêmes n’ont pas été reproduites.

Ensuite, la gravité varie beaucoup. L’injection Mime est notée 4,9 en CVSS v4 par GitHub et demande un usage précis du composant. Les trois autres sont des DoS : elles arrêtent un processus PHP, elles n’exécutent pas de code. Une borne de taille d’en-tête au reverse proxy et une limite de taille de corps de requête réduisent l’exposition au DoS de HttpFoundation, sans la supprimer.

Enfin, le contre-argument le plus sérieux : « attendre quelques jours après un correctif évite justement ces reprises ». C’est vrai pour une régression fonctionnelle, faux pour une faille exploitable sans authentification. Je préfère mettre à jour vite et tester ce qui compte, plutôt que différer en espérant que la version suivante soit la bonne.

Lundi matin : quatre vérifications

  1. Lancez composer audit --locked sur chaque application, et ajoutez-le en CI s’il n’y est pas : c’est aujourd’hui la source la plus à jour pour ces avis.
  2. Montez sur 5.4.56, 6.4.50, 7.4.23 ou 8.1.11 selon la branche ; si vous êtes en 8.0, planifiez le passage en 8.1 plutôt qu’un correctif qui n’existe pas.
  3. Cherchez les usages de HeaderUtils::split() avec un séparateur espace si vous avez traversé 6.4.46 à 6.4.47, 7.4.19 à 7.4.20 ou 8.1.7 à 8.1.8.
  4. Passez le grep ci-dessus pour décider quelles applications traiter en premier, et restreignez acceptFormat sur les endpoints qui n’ont aucune raison d’accepter du YAML.

La prochaine fois qu’un avis tombe, la version à retenir n’est peut-être pas celle qu’il cite, mais celle que votre outil d’audit accepte encore une semaine plus tard.

Mise à jour du 5 novembre 2026

Le 4 novembre, Symfony a publié 5.4.56, 6.4.50, 7.4.23 et 8.1.11, accompagnées de huit avis de sécurité. Deux d’entre eux illustrent la thèse de cet article : CVE-2026-118513 reprend le correctif incomplet de l’authentificateur X509 (CVE-2026-45063), et l’avis GHSA-hwfg-qwvh-2wfx sur le parseur YAML signale qu’un des cas de lenteur est apparu avec 6.4.49, 7.4.22 et 8.1.10, les versions que je recommandais ici. La version minimale de chaque branche est donc désormais 5.4.56, 6.4.50, 7.4.23 ou 8.1.11 : le tableau, le paragraphe sur la 8.0 et la deuxième vérification ont été corrigés en conséquence. Si une application associe #[MapRequestPayload] à un DTO qui porte une propriété UploadedFile, traitez-la en premier : l’avis CVE-2026-118515 décrit comment une requête sans aucun upload peut faire traiter n’importe quel fichier lisible du serveur comme un fichier envoyé.

Sources

Sur ce sujet, l’offre de l’atelier : Migration Symfony

À lire aussi

Un socle à migrer, une architecture à fiabiliser ?

Renfort senior en régie, ou forfait à périmètre et prix fixés : l’atelier intervient à distance sur PHP, Symfony, les systèmes distribués et l’adoption des agents de code, avec des tests de caractérisation avant chaque migration.