El miércoles, mientras andaba con los parches de PHP2Shell, Node.js publicaba otro aviso crítico: CVE-2026-111497, bautizado como node2shell. Parecía un problema de Node, y lo dejé para después. Ha sido un error, porque el fallo no está en Node sino en Brotli, la biblioteca de compresión de Google, y a lo largo de la semana ha ido apareciendo en Python, C++, Bun, Envoy, Istio, curl… Aikido lo resume en un artículo que ya habla de HTTP2Shell, porque lo que está en riesgo es casi cualquier cosa que hable HTTP con compresión.
Y al revisarlo, resulta que la imagen Docker que actualicé el miércoles para PHP2Shell también era vulnerable.
Qué es Brotli y qué ha fallado
Brotli es un formato de compresión sin pérdida que se usa mucho en HTTP para comprimir HTML, CSS y JavaScript: es el br que aparece en las cabeceras Accept-Encoding y Content-Encoding. Lo implementa una biblioteca en C, y casi todo el mundo usa esa misma biblioteca, empaquetada de una forma u otra.
El fallo es una escritura fuera de límites en el decodificador, en las versiones 1.0.3 a 1.2.0, con una puntuación CVSS v4 de 9,2. Está en la función DecodeBlockTypeAndLength(), que gestiona los cambios de bloque dentro del flujo comprimido. En las versiones vulnerables devuelve un booleano, y false puede significar dos cosas muy distintas: “estos datos están mal formados” o “necesito más datos”. Quien la llama no sabe distinguirlas, y en la ruta rápida ni siquiera mira el resultado. Un flujo preparado a propósito puede forzar un cambio de bloque imposible, y el decodificador sigue trabajando con un estado que ya no es válido hasta escribir fuera del búfer.
Lo que lo hace grave es que funciona en los dos sentidos:
- Un cliente malicioso puede atacar un servidor que descomprima cuerpos de petición en Brotli.
- Un servidor malicioso puede atacar a un cliente que acepte respuestas comprimidas.
Imperva ha publicado una prueba de concepto que ejecuta código con la imagen oficial de Node.js 24.22.0, tanto con el fetch() integrado al recibir una respuesta como con Express 5 y body-parser 2 al recibir una petición (sus parsers raw() y json() descomprimen Brotli). Con 24.22.1, las mismas entradas se rechazan. Un servidor HTTP de Node sin más no descomprime los cuerpos que recibe, así que el riesgo depende de qué middleware uses.
Lo curioso: el arreglo llevaba meses en el código
OpenAI encontró el fallo en febrero y lo reportó como ejecución remota de código. La causa se corrigió entonces en el repositorio de Brotli (el commit 0ed4268), pero no se trató como un problema de seguridad y no salió ninguna versión con el arreglo hasta ahora. Brotli 1.2.1, según quienes han comparado el código, sólo cambia el número de versión y ese commit.
Es un patrón que se repite: corregir un fallo no sirve de mucho si no llega a una versión publicada, y cuando la biblioteca va incluida dentro de otros proyectos, cada uno tiene que publicar su propia actualización. Por eso la lista de afectados ha ido creciendo día a día.
Versiones corregidas
| Proyecto | Versiones corregidas |
|---|---|
| Brotli (original) | 1.2.1, y 1.0.10 y 1.1.1 para ramas anteriores |
| Node.js | 22.23.4, 24.22.1, 26.12.1 |
Python (Brotli, brotlicffi) | 1.2.1 y 1.2.1.0 |
| Bun | 1.4.4 |
| Envoy | 1.40.0, 1.39.3, 1.38.6, 1.37.8 |
| Istio | 1.29.9, 1.30.6, 1.31.2 |
Apache con mod_brotli y Nginx no descomprimen Brotli por defecto (lo usan para comprimir), así que el servidor web en sí no suele ser el problema. Lo que hay detrás sí puede serlo.
En Python hay un detalle importante: requests, httpx o aiohttp cargan Brotli o brotlicffi automáticamente si están instalados, aunque tu código no los use directamente. Y según Aikido, a día 23 las bases de datos de avisos todavía no asociaban el CVE a esos paquetes de PyPI, así que pip-audit, Dependabot o Safety no te van a avisar todavía. Hay que comprobarlo a mano:
pip list 2>/dev/null | grep -i brotli
Mi imagen de PHP también lo tenía
Hace cuatro días contaba cómo había actualizado drzippie/php-nginx, mi imagen con PHP-FPM y Nginx sobre Alpine, para corregir PHP2Shell. Pensaba que Brotli no iba conmigo, pero la imagen incluye Node.js para compilar recursos, y en Alpine Node no lleva su propia copia de Brotli sino que usa la del sistema. Así que lo comprobé:
docker run --rm --entrypoint sh drzippie/php-nginx:2026-10-21 -c \
'node -p process.versions.brotli; apk info -v | grep brotli; ldd $(which node) | grep brotli'
1.2.0
brotli-libs-1.2.0-r1
libbrotlidec.so.1 => /usr/lib/libbrotlidec.so.1
Brotli 1.2.0, vulnerable. Y no sólo la usa Node. Buscando qué más enlaza libbrotlidec dentro de la imagen, la lista es larga:
libcurl, y con ella la extensióncurlde PHP, que descomprime las respuestasbr.- Swoole, cuyo cliente HTTP también acepta respuestas comprimidas.
- La extensión
brotlide PHP, conbrotli_uncompress(). - FreeType, que usa Brotli para las fuentes WOFF2, y a través de ella GD e ImageMagick.
Todos los caminos llevan al mismo decodificador.
En mi caso el riesgo real es bajo, porque Node sólo se usa al construir y mis aplicaciones PHP no descomprimen cuerpos Brotli que les envíe nadie. Pero una petición con curl a un servidor externo que responda con Content-Encoding: br sí pasa por ese decodificador, y no me apetece depender de que ningún servicio externo sea malicioso.
La buena noticia es que Alpine 3.24 ya tiene brotli-libs 1.2.1 y nodejs 24.22.1, así que no hace falta compilar nada. Basta con reconstruir la imagen sin caché, para que no reutilice capas con los paquetes antiguos:
docker build --pull --no-cache -t drzippie/php-nginx .
Y comprobarlo:
v24.22.1
1.2.1
brotli-libs-1.2.1-r0
nodejs-24.22.1-r0
De paso, Alpine ya ha empaquetado PHP 8.4.27, así que la etapa que añadí para compilar PHP por mi cuenta ya no hace falta y la imagen vuelve a usar el paquete oficial.
| Etiqueta | PHP | Brotli | Node.js | Notas |
|---|---|---|---|---|
latest, 2026-10-25 | 8.4.27 | 1.2.1 | 24.22.1 | Corrige también CVE-2026-111497 |
2026-10-21 | 8.4.27 | 1.2.0 | 24.18.1 | Corrige PHP2Shell, vulnerable a HTTP2Shell |
Lo que me llevo de esto es que el --no-cache importa. Si reconstruyes una imagen con caché, Docker puede reutilizar la capa del apk add y dejarte exactamente la misma biblioteca vulnerable con una etiqueta nueva.
Si no puedes actualizar todavía
Aikido propone algunas medidas para ganar tiempo:
- Rechazar peticiones con
Content-Encoding: bren el proxy, el balanceador o el middleware, antes de que el cuerpo llegue al decodificador. Ojo con las mayúsculas y con las listas de codificaciones. - En las peticiones salientes, quitar
brdeAccept-Encodingno basta: un servidor malicioso puede enviar Brotli igualmente, y librerías como urllib3 eligen el decodificador según la cabecera de la respuesta. Hay que desactivar la descompresión Brotli o rechazar esas respuestas antes de procesarlas. - En Python, si Brotli es opcional, desinstalar
Brotliybrotlicffiy quitar extras comohttpx[brotli]. Las librerías siguen funcionando con gzip y deflate.
Y después, lo de siempre: actualizar los ficheros de bloqueo de dependencias, reconstruir las imágenes, desplegar y comprobar que lo que está corriendo usa la versión corregida, no sólo lo que dice el Dockerfile.
Lo que me llevo
Esta semana ha tenido dos fallos críticos con mecánicas muy distintas, pero con una lección común. En PHP2Shell, el problema estaba en el lenguaje que uso todos los días y era fácil saber si me afectaba. En HTTP2Shell, estaba en una biblioteca de compresión que no he elegido nunca de forma consciente y que entró en mi imagen por media docena de caminos distintos. Saber qué llevan dentro tus contenedores, más allá de lo que pusiste en el Dockerfile, es lo que permite responder en minutos a la pregunta “¿esto me afecta?”.
Fuentes: Node2Shell evolves to HTTP2Shell de Aikido, Node2Shell: From Brotli Data to Remote Code Execution de Imperva y el commit de la corrección en Brotli.




