CVE-2026-111497: el fallo de Brotli que rompe Node.js, Bun y Python
El 21 de octubre de 2026 se publicó una actualización de seguridad en Node.js que, en el fondo, no tenía nada que ver con Node.js. El fallo estaba en el decodificador de Brotli de Google, la biblioteca en C que comprime y descomprime tráfico web, y esa copia de código viaja dentro de muchísimos programas distintos. Lo que me puso las alertas fue otra cosa: en mi propia estación de trabajo el paquete libbrotli1 seguía en 1.1.0-2+b7 mientras el repositorio de seguridad de Debian ya tenía el 1.1.0-2+deb13u1 esperando, y el curl del sistema anunciaba brotli/1.1.0 en su línea de features. Solo una actualización de sistema pendiente y ya estaba expuesta sin saberlo.
Este texto es la guía que me faltó a mí esa tarde: cómo averiguar si tu servidor decodifica Brotli de origen no confiable, qué versiones arreglan el problema en Node.js, Bun, Python, Envoy e Istio, y por qué quitar br del header Accept-Encoding no sirve de nada por sí solo.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 20 minutos de auditoría con los comandos de este post, más el reinicio de cada servicio que decodifique Brotli.
- Requisitos: acceso root o sudo al servidor, el inventario de tus servicios Node.js, Bun y Python, y la lista de contenedores que corres en producción.
- Resultado: sabrás qué componente tuyo decodifica Brotli de fuera, a qué versión parcheada tienes que subir y en qué orden hacerlo.
- Troubleshooting: si no puedes parchear hoy, rechaza
Content-Encoding: bren el borde y desactiva la decodificación automática en los clientes HTTP.
Qué es CVE-2026-111497 y dónde vive exactamente el fallo
Brotli es el formato de compresión que Google publicó en 2015 y que hoy usan casi todos los servidores web para comprimir HTML, CSS, JavaScript y respuestas de API. La biblioteca oficial está escrita en C, en el archivo c/dec/decode.c, y cada programa que la usa lleva su propia copia: Node.js la empaqueta dentro del binario, Python la compila como extensión nativa, Envoy la lleva dentro de su proxy. Eso es lo que convierte a un solo bug de memoria en un problema transversal: no hay un parche central que empuje a todos.
El identificador es CVE-2026-111497, con CWE-787 (escritura fuera de límites) y CWE-252 (valor de retorno no verificado) según el registro de NVD. La versión corregida de la biblioteca es la 1.0.10, la 1.1.1 y la 1.2.1, que cierran el rango afectado de 1.0.3 hasta 1.2.0.
El mecanismo es fino pero entendible. Dentro del decodificador hay una función llamada DecodeBlockTypeAndLength() que se encarga de los saltos de bloque, es decir, de los puntos donde un flujo Brotli cambia de árbol de Huffman. En las versiones vulnerables esa función devolvía un valor booleano y ese booleano era ambiguo: BROTLI_FALSE podía significar “el flujo está mal formado” o “necesito más entrada para continuar”. En la ruta rápida del decodificador, ese valor de retorno se descartaba sin comprobarlo. Un flujo comprimido construido a mano puede declarar un solo tipo de bloque, agotar la longitud de ese bloque y forzar un salto de bloque que no puede completarse. El decodificador terminaba siguiendo con el estado de bloque que ya tenía y escribiendo más allá del límite del buffer circular.
El parche upstream es el commit 0ed4268a de google/brotli, con el título “Detect and report invalid stream earlier”. Cambia el tipo de retorno de la función a BrotliDecoderErrorCode y agrega un código de error nuevo, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH con valor -17, que antes estaba reservado. El detalle importante para operar: el arreglo entró al código fuente de Brotli el 27 de febrero de 2026, pero ninguna release publicada lo llevó al 21 de octubre: casi ocho meses de código corregido sin distribuir. Los cherry-picks a las ramas de mantenimiento (1.0.10, 1.1.1 y 1.2.1) se hicieron el mismo 21 de octubre, así que las ramas vivas recibieron el arreglo el día del lanzamiento. OpenSourceMalware verificó el comportamiento compilando la 1.2.0 y la 1.2.1 con AddressSanitizer: contra un flujo de unos 23 MB, la 1.2.0 consumía el flujo completo y seguía reportando que necesitaba más entrada, mientras la 1.2.1 lo rechazaba siempre con el error -17.
Sobre el impacto hay que ser honesto con las cifras. Google, que actúa como autoridad de nominación del CVE, le asignó un CVSS 4.0 de 9.2 (crítico) con vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. Node.js lo clasificó como high en su release de seguridad. Red Hat lo ratea como Important con 8.1 en CVSS 3.1, argumentando que sus hardenings de plataforma (ASLR, NX, FORTIFY_SOURCE, SELinux) suben la barra del ataque y que no existe un proof-of-concept público y confiable que ejecute código. Imperva consiguió ejecutar código en un build de Node.js 24.22.0 para Linux sobre ARM64, tanto inyectando la respuesta de un fetch() como en el cuerpo de una petición de Express. No hay explotación reportada en producción.
Qué software está afectado y qué versiones lo corrigen
Esta tabla la armé cruzando el advisory de Envoy, el registro de NVD, el índice oficial de distribuciones de Node.js, las notas de release de Bun, las entradas PYSEC del ecosistema Python y la guía de divulgación de OpenAI, que es la fuente de las cifras de Istio. Es la lista que uso para responder “¿estoy afectado?” en menos de un minuto:
| Producto | Versiones afectadas | Versión corregida | Condición de exposición |
|---|---|---|---|
| Google Brotli (librería C) | 1.0.3 hasta 1.2.0 | 1.0.10, 1.1.1, 1.2.1 | Decodifica datos Brotli no confiables |
| Node.js | 22.x, 24.x y 26.x anteriores al parche; 20.x y anteriores (fuera de soporte) | 22.23.4, 24.22.1, 26.12.1 | fetch() de una respuesta br, o cuerpos br en Express 5 y body-parser 2 |
| Bun | 1.4.3 y anteriores | 1.4.4 (incluye Brotli 1.2.1) | fetch() de una respuesta br, o el módulo node:zlib |
| Envoy | 1.39.2, 1.38.5, 1.37.7, 1.36.11 | 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12 | Solo si el decompressor de Brotli está configurado |
| Istio | 1.29.0 a 1.29.8, 1.30.0 a 1.30.5, 1.31.0 a 1.31.1 | 1.29.9, 1.30.6, 1.31.2 | Solo si la descompresión Brotli está habilitada (1.29 ya está fuera de soporte) |
Python Brotli (PyPI) |
1.2.0 y anteriores | 1.2.1 | El paquete está instalado y un cliente HTTP decodifica br |
Python brotlicffi (PyPI) |
1.2.0.x y anteriores | 1.2.1.0 | El paquete está instalado y un cliente HTTP decodifica br |
| cdxgen (binarios e imágenes) | 13.4.0 y anteriores; 12.8.8 y anteriores | 13.5.0 y 12.8.9 | Escanear un repositorio con contenido controlado o usar el modo servidor |
Dos matices que no aparecen en la tabla y que valen la pena. Primero: un servidor HTTP de Node.js puro no decodifica cuerpos de petición por su cuenta, el vector de entrada es el fetch() saliente o un parser de cuerpo como los de Express 5 y body-parser 2, que Imperva probó en las versiones 5.0.0 a 5.3.0 y 2.0.0 a 2.3.0 respectivamente. Segundo: en Python no hace falta que tu código importe nada. Con el paquete Brotli o brotlicffi presente en el entorno, clientes como urllib3 (y por lo tanto requests), httpx con el extra brotli y Scrapy anuncian Accept-Encoding: br y decodifican la respuesta automáticamente; aiohttp hace lo mismo cuando Brotli está instalado. Tu servicio queda expuesto sin haber escrito una sola línea con la palabra brotli. En Bun hay además un tercer API afectado que la tabla no menciona: DecompressionStream con formato brotli.
Revisar si tu servidor está expuesto
Empieza por el runtime. Estos comandos no modifican nada y te dan el punto de partida: con qué versión de Node.js o Bun corres, y si el binario activo es una de las versiones parcheadas.
node --version
bun --version 2>/dev/null || echo "Bun no instalado en este host"
node -p "process.versions.node"
El siguiente paso es Python, que es el caso más silencioso de todos. Revisa la versión instalada y, sobre todo, quién la está arrastrando como dependencia transitiva, porque casi nunca es tu código el que la pide explícitamente.
pip list | grep -iE "Brotli|brotlicffi"
pip show Brotli
pipdeptree -r -p Brotli
Y después, la capa del sistema operativo. Ojo con este punto, que es el que más confunde: el número de versión del paquete de Debian no cambia al aplicar el parche. El libbrotli1 sigue llamándose 1.1.0, y lo que cambia es el sufijo de revisión de Debian. El commit upstream que arregla la 1.1.x es da980636 y Debian lo lleva backportado.
dpkg -l libbrotli1
apt-cache policy libbrotli1
apt list --upgradable 2>/dev/null | grep -i brotli
curl --version | head -1
La línea de curl --version importa, pero no como indicador de si estás parcheado. Ojo con esto porque es la trampa del día: el parche de Debian no sube la versión de la librería, solo modifica el decodificador, así que curl --version seguirá anunciando brotli/1.1.0 idéntico antes y después del parche. Lo que sirve es el sufijo de revisión del paquete (1.1.0-2+deb13u1 frente a 1.1.0-2+b7) y el hecho de que el curl del sistema enlace libbrotlidec.so.1 de forma dinámica: cuando esa biblioteca se actualiza, el curl queda reparado en el siguiente reinicio del proceso.
Lo que sí es cierto es que tu curl es un vector de entrada: decodifica respuestas br de servidores que no controlas. OpenAI incluye a curl y libcurl en la lista de software que hay que revisar, con la instrucción de “revisar el decoder que carga curl/libcurl”. Pero que quede claro lo que eso significa y lo que no: no existe un exploit público que demuestre ejecución de código en curl, y Red Hat lo dice de forma explícita, “there is no public, reliable remote code execution proof-of-concept”. Y el flujo disparador no es el primer byte: el que construyó OpenSourceMalware ocupa unos 23 MB.
Para los contenedores, la imagen es la unidad de despliegue y no el contenedor: si la imagen se construyó con una versión vieja, sigue vulnerable aunque la arregles “adentro” y la reinicies.
docker ps --format "{{.Names}} {{.Image}}"
docker exec api python -c "import brotli; print(brotli.__version__)"
Actualizar Node.js y Bun
Las versiones corregidas son la 22.23.4, la 24.22.1 y la 26.12.1 para las líneas 22.x, 24.x y 26.x. Ojo con un detalle del índice de distribuciones: la 22.23.4 y la 24.22.1 están fechadas el 20 de octubre y la 26.12.1 el 21, y esta última es la línea Current, no una LTS. Si corres Node.js 20.x o anterior, ese ciclo ya está fuera de soporte: no hay versión corregida publicada y la única salida es migrar a una línea mantenida. Para Bun, la 1.4.4 es la que trae el decodificador 1.2.1 dentro.
Si tu servicio corre en systemd, actualizar el binario no alcanza: el proceso en memoria sigue usando el decodificador viejo hasta que se reinicia. Este es el punto donde se repiten los incidentes, porque la actualización sí se aplicó pero nadie reinició el servicio.
systemctl list-units --type=service --state=running | grep -iE "node|bun|api"
sudo systemctl restart api.service
systemctl status api.service --no-pager
Con Bun la actualización es un comando, pero de nuevo: reinicia el proceso.
bun upgrade
bun --version
Para los contenedores de Node.js, reconstruye la imagen y vuelve a desplegar. Un docker restart sobre la imagen vieja no arregla absolutamente nada, porque el decoder viejo viaja dentro de la imagen.
docker build -t api:seguro .
docker push registry.example.com/api:seguro
Actualizar los paquetes Python Brotli y brotlicffi
Aquí la actualización es directa y no tiene misterio: Brotli 1.2.1 y brotlicffi 1.2.1.0. Hay un detalle operativo que casi nadie mira y que te puede ahorrar una tarde entera: los release notes de esas dos versiones no mencionan el CVE. Y el mapeo del CVE contra los paquetes de PyPI llegó tarde: el advisory de GitHub GHSA-9v8h-qqgm-4m35 ya existía desde el 22 de octubre, pero hasta el 23 el CVE no estaba asociado a los paquetes en OSV ni en el tooling, porque la entrada del CVE apunta al repositorio de Google y no a PyPI. Eso significa que pip-audit, Dependabot y Safety no te iban a avisar. Las entradas PYSEC-2026-4322 y PYSEC-2026-4323 se publicaron ese mismo 23 de octubre y ya cubren el caso, pero la auditoría manual sigue siendo obligatoria.
pip install --upgrade "Brotli==1.2.1"
pip install --upgrade "brotlicffi==1.2.1.0"
python3 -c "import brotli; print(brotli.__version__)"
Para el caso en que uses un archivo de dependencias bloqueado, con Poetry, uv o pip-tools, no basta con cambiar el requisito: hay que regenerar el lockfile y volver a construir la imagen. Y de nuevo, verifica la versión final dentro del contenedor, no solo en tu máquina.
docker run --rm --entrypoint pip api:seguro show Brotli
Actualizar la librería del sistema en Debian y RHEL
En Debian el paquete brotli ya está corregido en las dos ramas estables: 1.0.9-2+deb12u1 en bookworm, con el aviso DLA-4855-1, y 1.1.0-2+deb13u1 en trixie, con el aviso DSA-6582-1. En forky y sid la versión 1.2.0-4 sigue marcada como vulnerable y sin corregir, con el bug abierto 1151690. Mi estación tenía el 1.1.0-2+b7 de la rama principal y el 1.1.0-2+deb13u1 esperando en el repositorio de seguridad: un apt upgrade normal lo résolvía. Y comprobé el resultado a nivel binario: la libbrotlidec.so.1 instalada no contenía la cadena del código de error nuevo, mientras que la del paquete parcheado sí. El parche de Debian es el cherry-pick da980636 del commit upstream 0ed4268a.
sudo apt-get update
sudo apt-get install --only-upgrade libbrotli1
apt-cache policy libbrotli1
Y aquí viene el punto donde la mayoría se equivoca, porque la respuesta depende de cómo quedó compilado tu Node.js. El nodejs de Debian enlaza el decodificador de forma dinámica contra libbrotlidec.so.1 y declara Depends: libbrotli1 (>= 1.0.0): verifiqué el libnode115 de trixie y sus símbolos BrotliDecoder* quedan sin definir (UND), es decir, se resuelven en tiempo de ejecución contra la librería del sistema. En ese caso un apt upgrade libbrotli1 más un reinicio del proceso Node.js sí te protege, sin tocar el runtime. Los builds oficiales de Node.js y los de NodeSource hacen lo contrario: compilan Brotli dentro del ejecutable, así que su node declara el símbolo como definido (T) y no enlaza la librería del sistema. Esos sí exigen subir a 22.23.4, 24.22.1 o 26.12.1. Para saber en qué caso estás, mira el binario:
readelf -d /usr/bin/node | grep -i brotli
readelf -sW /usr/bin/node | grep -m1 BrotliDecoderDecompressStream
Si la segunda línea muestra UND, estás con el paquete de la distribución y el parche de la librería te cubre. Si muestra T o Func definido, el decoder va dentro del binario y solo te salva cambiar de versión de Node.js. El tracker de seguridad de Debian marca el paquete nodejs como vulnerable en bookworm, trixie, forky y sid, con la entrada de unstable como unfixed y urgencia unimportant, precisamente porque el arreglo llega por la librería del sistema y no por un rebuild del runtime.
En Red Hat, la lista de paquetes afectados incluye python-brotli en Red Hat Update Infrastructure 4 for Cloud Providers y en Red Hat Satellite 6 (python-brotli y python3.12-brotli), además de componentes de OpenShift Container Platform 4. Red Hat califica el problema como Important y recomienda limitar la exposición de los servicios que descomprimen Brotli de pares no confiables y restringirles el acceso de red.
Mitigar cuando todavía no puedes actualizar
Si el parche no entra hoy, el objetivo es que los datos maliciosos no lleguen al decoder. Hay tres puertas que cerrar, y el orden importa.
El primero es la entrada: rechaza peticiones que llegan con Content-Encoding: br antes de que cualquier proxy, middleware o parser de cuerpo las descomprima. Tiene que ser en el borde, no en la aplicación, porque si el proxy ya descomprimió el cuerpo, la aplicación nunca ve el header original.
El segundo es la salida: desactiva la descompresión automática en los clientes HTTP y rechaza respuestas Brotli antes de decodificarlas. Y ojo con el malentendido más caro de esta vulnerabilidad: quitar br del header Accept-Encoding no protege. Un servidor malicioso puede mandar Content-Encoding: br de todos modos, y urllib3 decide basándose en el header de la respuesta, no en lo que pediste. OpenAI lo dice de forma explícita en su guía de parcheo.
El tercero son los proxies. En Envoy e Istio, desactiva el filtro decompressor de Brotli y deja activos los de gzip y zstd, que no están afectados. Ojo con un detalle: si quitas el filtro, los bytes comprimidos siguen de paso hacia adelante, así que hay que proteger también los decoders que quedan aguas abajo.
grep -rn "decompressor" /etc/envoy /etc/istio 2>/dev/null
grep -rn "decompressor" /etc/kubernetes/manifests 2>/dev/null
En Node.js, el advisory del proyecto no publica un workaround propio, porque su recomendación es actualizar el runtime. Pero la guía de OpenAI sí propone mitigaciones aplicables sin esperar al parche: un guard de codificación que se registre en Express antes de cualquier body parser para rechazar cuerpos Brotli con estado 415, el fallback inflate: false en los parsers existentes, y images.unoptimized en Next.js para que el optimizador de imágenes deje de descargar y decodificar recursos remotos. Imperva y Vercel publicaron además reglas de WAF el mismo día de la divulgación. En Python sí hay una salida limpia, que es desinstalar los dos paquetes para que los clientes caigan de vuelta a gzip y deflate. Si borras Brotli pero dejas un extra tipo httpx[brotli] en tus requisitos, la siguiente instalación lo devuelve, así que hay que limpiar las dos cosas.
pip uninstall -y Brotli brotlicffi
grep -riE "brotli" requirements.txt pyproject.toml poetry.lock uv.lock 2>/dev/null
Y una advertencia que aplica para todo lo demás: un WAF ayuda solo hasta cierto punto. Vercel publicó reglas de firewall el mismo día de la divulgación, pero su propia nota dice que esas reglas no cubren “las respuestas que tus funciones descargan de otros servidores” (traducción propia del original en inglés). Ese camino, el cliente saliente, solo se cierra actualizando el runtime o la biblioteca.
Verificar que la corrección quedó aplicada
Un parche instalado no es un parche activo. Verifica en tres planos: la versión del archivo en disco, la del proceso en ejecución y la del contenedor desplegado. El curl con Accept-Encoding: br te confirma que el camino sigue vivo en el servidor, que es justo lo que quieres comprobar antes de cerrar la tarea.
dpkg -l libbrotli1 | grep brotli
node --version
curl -sI -H "Accept-Encoding: br" https://tu-servidor.example/ | grep -i "content-encoding"
En el caso de Python, una comprobación rápida del entorno de runtime te dice si la extensión cargada es la nueva:
python3 -c "import brotli; print(brotli.__version__)"
pipdeptree -r -p Brotli
Prioridades de ataque: qué resolver primero si tienes 30 minutos
Con la información dispersa entre un advisory de Envoy, dos entradas PYSEC, el tracker de Debian y las notas de release de Node.js, el orden de ataque importa más que la profundidad del análisis. Empieza por lo que expone la red, no por lo que te da más puntos en el escáner.
Minuto 0 a 5. Revisa si tienes proxies con descompresión Brotli activa: Envoy, Istio, o cualquier CDN o balanceador que lo tenga como filtro. Un proxy expuesto es el peor caso, porque el decoder vive en el camino de todas las peticiones y un exploit en ese punto afecta a todo lo que atraviesa. Si tienes Envoy, sube a 1.40.0 o a la última 1.39.x, 1.38.x o 1.37.x parcheada; si tienes Istio, sube a 1.31.2 o 1.30.6.
Minuto 5 a 15. Actualiza Node.js y Bun en los servicios que hacen llamadas salientes. Este es el escenario de RCE demostrado por Imperva, aunque sea el único. Recuerda: reiniciar el servicio, no solo actualizar el binario.
Minuto 15 a 25. Barre Python. Es donde está la sorpresa: Brotli y brotlicffi son dependencias transitivas en muchísimos proyectos y ninguno de tus escáneres te los iba a marcar. Usa pipdeptree para saber quién los arrastra, y sube a 1.2.1 y 1.2.1.0.
Minuto 25 a 30. Aplica la mitigación de borde: rechaza Content-Encoding: br en el proxy de entrada. Es una línea de configuración que reduce la superficie mientras terminas el resto, y no requiere reiniciar el runtime.
Y una recomendación más de fondo: si tienes un solo lugar donde decides qué software corre en producción, ese lugar va a ser tu mejor defensa contra la próxima CVE de este tipo. Los tiempos de exposición de un CVE que se extiende a un proyecto entero como este dependen de la velocidad con la que puedes actualizar ese inventario, no de la velocidad con la que lees el advisory.
Preguntas frecuentes
¿Estoy expuesto si solo uso Brotli para comprimir mis respuestas?
No, si nunca desactivas la entrada. El fallo está en el decoder, no en el encoder: solo se activa cuando el programa descomprime datos Brotli que viene de afuera. Un servidor que comprime lo que envía y nunca descomprime peticiones ni respuestas ajenas no está en la ruta del ataque, aunque use una versión vieja de la biblioteca.
¿Basta con quitar br del header Accept-Encoding para proteger mi servicio?
No, y es el error más caro de esta lista. Un servidor controlado por el atacante puede responder con Content-Encoding: br aunque tu cliente no lo pidió, y los clientes como urllib3 o requests deciden descomprimir según el header de la respuesta, no según lo que anunciaste en la petición. La protección real está en rechazar la respuesta Brotli antes de decodificarla o en actualizar la biblioteca.
¿Cómo verifico si un contenedor mío trae una versión vulnerable de Brotli?
La imagen es la unidad vulnerable, no el contenedor en ejecución. Ejecuta pip show Brotli dentro del contenedor para ver la versión que realmente se carga, y reconstruye la imagen desde cero si cambias la versión: un simple docker restart sobre la imagen vieja sigue ejecutando el decoder viejo. En Python el valor debe quedar en 1.2.1 o superior para Brotli.
Uso Debian y actualicé libbrotli1, ¿ya estoy protegido?
Depende de qué corra tu Node.js. Depende de qué Node.js tengas. El nodejs de Debian enlaza el decodificador dinámicamente contra libbrotlidec.so.1, así que actualizar libbrotli1 y reiniciar el proceso sí te protege. Los builds oficiales y los de NodeSource empaquetan su propia copia del decoder, y esos sí necesitan que subas a 22.23.4, 24.22.1 o 26.12.1. El tracker de Debian marca nodejs como vulnerable en bookworm, trixie, forky y sid porque el arreglo llega por la librería del sistema, no por un rebuild del runtime.
¿Node.js y Bun comparten el mismo código vulnerable?
Sí, la misma base. Node.js empaqueta el decodificador de Google dentro del binario y Bun lo incluye en su build, por eso la 1.4.4 de Bun ya trae la 1.2.1 de Brotli. En Node.js, el advisory del proyecto no publica un workaround propio y recomienda actualizar el runtime a la 22.23.4, 24.22.1 o 26.12.1, aunque la guía de OpenAI sí propone mitigaciones previas: un guard de codificación en Express antes de los parsers, inflate: false como fallback e images.unoptimized en Next.js. En Python, en cambio, desinstalar los dos paquetes sí es una mitigación válida.
Lee también
- Últimas vulnerabilidades: Ivanti, Cisco, Palo Alto y ahora BeyondTrust en la mira
- QPACK: una sola conexión HTTP/3 puede agotar la memoria de tu servidor
- MALFEX imgdecode: el paquete npm que mete shellcode en un add-on nativo de Node.js
- Peligros ocultos en Linux: cómo funciones comunes se vuelven vectores de ataque
- Cómo funciona QUIC y por qué tu servidor HTTP debería hablarlo en 2026
