Symfony ExpressionLanguage: parchea CVE-2026-109449 por rama
Cómo inventariar ExpressionLanguage, elegir el parche Symfony correcto por rama y validar el rechazo controlado sin publicar un exploit.
15 de octubre de 2026 · Leopoldo Benavente Cadena · 6 min de lectura
El aviso de Symfony del 15 de octubre de 2026 describe CVE-2026-109449 como un problema de disponibilidad en symfony/expression-language. El parser construía árboles de nodos sin un límite de profundidad; una expresión no confiable con anidación extrema podía hacer caer el proceso PHP al liberar ese árbol. Eso no equivale a ejecución de código, acceso a datos ni bypass de autenticación.
La corrección cuenta la profundidad y produce SyntaxError al superar 256 niveles. El aviso describe árboles formados por operadores unarios, literales de array, operadores binarios o accesos a propiedades; no hace falta publicar esas entradas para entender el riesgo. También aclara que la primera reparación de 6.4.45, 7.4.18 y 8.1.6 no cubría toda la profundidad añadida por ciertas cadenas de operadores binarios y accesos a propiedades. Por eso no basta con recordar un parche anterior: hay que comprobar la versión efectiva y la rama del proyecto.
Comprueba la exposición sin ejecutar expresiones
Que symfony/expression-language aparezca en vendor/ no demuestra por sí solo que una solicitud pueda alcanzar el parser. Puede ser una dependencia transitiva, una herramienta de consola o una ruta que nunca acepta expresiones externas. Tampoco conviene concluir “no alcanzable” solo porque no encuentres #[Assert\ExpressionSyntax]: revisa controladores, validadores propios, formularios, workers, comandos y otros paquetes que llamen a ExpressionLanguage o a lint().
En una rama de mantenimiento, registra primero el estado sin cambiar el lockfile:
php --version
composer --no-plugins --no-scripts show symfony/expression-language
composer --no-plugins --no-scripts show symfony/validator
composer --no-plugins --no-scripts show symfony/framework-bundle
composer --no-plugins --no-scripts show --locked
composer --no-plugins --no-scripts why symfony/expression-language
composer --no-plugins --no-scripts audit --lockedCada composer show de paquete es un comando separado; no juntes varios nombres como si fueran versiones. composer show describe lo instalado en vendor/, mientras composer show --locked —si lo admite la versión de Composer del equipo— consulta lo resuelto en composer.lock. Contrasta ambos estados antes de decidir. audit --locked audita el lockfile; es una comprobación adicional y no sustituye el aviso del mantenedor ni una revisión de la aplicación.
Los flags --no-plugins --no-scripts son una variante de inventario: evitan cargar plugins y despachar scripts cuando la política del equipo lo permita, pero no deben aplicarse a ciegas si el proyecto necesita esos mecanismos para describir o construir el artefacto. Revisa allow-plugins y la sección scripts de composer.json antes de permitir su ejecución. Estos comandos de inventario no modifican por sí mismos el lockfile. No edites vendor/ ni uses una versión arbitraria para forzar una resolución; si la dependencia es transitiva, identifica qué paquete raíz impone la restricción.
Elige el parche de la rama existente
El aviso oficial enumera estos rangos y correcciones:
| Rama corregida mantenida | Afectada antes de | Corregida en | PHP mínimo de esta rama según Symfony Releases |
|---|---|---|---|
| 8.1 | 8.1.9 |
8.1.9 |
8.4 |
| 7.4 LTS | 7.4.21 |
7.4.21 |
8.2 |
| 6.4 LTS | 6.4.48 |
6.4.48 |
8.1 |
| 5.4 | 5.4.54 |
5.4.54 |
7.2.5 |
En términos de Composer, los rangos afectados del aviso son <5.4.54, >=6,<6.4.48, >=7,<7.4.21 y >=8,<8.1.9, respectivamente. Esos rangos son más amplios que las filas de destino: una aplicación en 6.0–6.3 no tiene una corrección de parche en esa rama y debe evaluar el salto a una rama mantenida. Mantén la rama que el proyecto ya soporta: Symfony 8.1 es la estable actual; 7.4 y 6.4 son LTS mantenidas; 5.4 recibe solo correcciones de seguridad. Los PHP mínimos de la tabla corresponden a cada rama corregida, no a todo el major 6, 7 u 8. Symfony 8.2 aparece como desarrollo y no es el destino de este parche.
La tabla no autoriza a migrar de 5.4 a 8.1 en la misma intervención. Revisa también la versión de PHP, extensiones, paquetes Symfony relacionados y restricciones de composer.json. El objetivo inmediato es alcanzar la versión corregida compatible, no convertir un aviso de seguridad en una migración mayor.
Actualiza en una rama de mantenimiento
La secuencia siguiente es conceptual y debe ejecutarse únicamente en una rama de mantenimiento, con copia de seguridad, revisión y el procedimiento de aprobación del equipo:
composer update symfony/expression-language --with-dependencies
composer show symfony/expression-language
composer auditDespués revisa el diff de composer.lock, confirma que la versión resuelta es la de tu rama y verifica que no se haya introducido una major inesperada. composer update modifica composer.lock y vendor/, y puede cargar plugins o ejecutar scripts; por eso requiere una rama de mantenimiento, un entorno controlado y autorización explícita, nunca una ejecución en producción. Si la política permite revisar la resolución sin esos efectos, usa los flags CLI --no-plugins --no-scripts en la variante pertinente, pero no supongas que deshabilitarlos siempre es válido para construir el artefacto final. Si la restricción del paquete raíz impide el parche, no lo ocultes con una edición manual: actualiza la restricción compatible o coordina la dependencia que bloquea la resolución.
Una reconstrucción de caché con entorno prod solo pertenece a un artefacto de laboratorio o staging que imite producción; no es una instrucción para limpiar la caché de producción desde el servidor:
php bin/console cache:clear --env=prodNo hay que afirmar que composer update elimina todos los riesgos del backend. PHP, extensiones, otros paquetes, límites de proxy, workers y controles de entrada siguen requiriendo revisión independiente.
Prueba un rechazo controlado, no un exploit
El protocolo propuesto usa un fixture sintético de profundidad limitada por encima del umbral documentado, generado dentro de un entorno de laboratorio. No es necesario publicar una cadena de decenas de miles de nodos ni enviarla a un proveedor externo. La prueba debe dirigirse solo a la ruta de la aplicación que realmente valida expresiones y usar datos que no contengan secretos.
Aceptación sugerida:
- la aplicación devuelve el código, error o violación de entrada inválida que su contrato ya define, en lugar de cerrar el proceso;
- se observa un
SyntaxErroren logs solo si la implementación lo captura o instrumenta; no es un requisito universal y nunca se copia la expresión completa ni datos sensibles; - el worker o proceso PHP sigue atendiendo una segunda solicitud independiente;
- una expresión normal continúa validándose y la caché del entorno de prueba se puede reconstruir;
- el fixture no se envía a producción.
Esto sería evidencia del manejo controlado en ese entorno, no una afirmación de cobertura de todas las rutas, bundles o versiones de PHP. En esta pieza no se ejecutó Composer ni una aplicación Symfony, por lo que los comandos y criterios son un protocolo para el equipo lector.
Cierra con evidencia y reversión
Guarda la versión de PHP, Composer y Symfony, el estado instalado y bloqueado, el diff de composer.lock, los resultados de la suite y el plan de rollback. Si una dependencia de terceros bloquea la actualización, abre el issue correspondiente y aplica solo mitigaciones operativas autorizadas; no inventes una configuración global que prometa neutralizar el parser.
El siguiente control debe revisar el aviso del mantenedor y volver a ejecutar composer audit después de nuevas publicaciones. El parche correcto es el de la rama que ya utiliza la aplicación: 5.4.54, 6.4.48, 7.4.21 u 8.1.9, según corresponda.
Fuentes oficiales: aviso CVE-2026-109449 de Symfony, versiones y soporte, Symfony 8.1.9, proceso de releases, CLI de Composer, plugins de Composer y instalación segura de paquetes no confiables.
Comentarios