1. Descripción técnica
HTTP2Shell, Node2Shell y Broccoli2Shell son nombres usados para CVE-2026-111497, un out-of-bounds write en el Google Brotli C decoder. Un malformed compressed input puede hacer que el decoder escriba más allá de un buffer interno. El resultado puede ser un crash, memory disclosure o Remote Code Execution (RCE). El aviso de Node.js clasifica la vulnerabilidad como alta; la puntuación CVSS v4.0 difundida para el fallo es 9.2, crítica. Aviso oficial de Node.js, GitHub Advisory Database, advisory de Palo Alto Networks.
Explicación corta
Brotli comprime datos para que ocupen menos y después los reconstruye al descomprimirlos. Un stream malformado puede declarar que sólo existe un block type y, más tarde, solicitar un block switch hacia otro tipo. Esa operación es imposible. La versión vulnerable devuelve un false ambiguo y, en el fast path, una capa intermedia incluso descarta ese return value. El decoder continúa o reanuda el procesamiento con un invalid state y termina provocando un out-of-bounds write. Si una aplicación descomprime contenido controlado por un atacante, se produce memory corruption dentro de su process y con los permisos de la cuenta que lo ejecuta.
El nombre Node2Shell surgió porque el fallo se divulgó primero en la copia de Brotli incluida en Node.js. HTTP2Shell describe las rutas donde una petición o respuesta HTTP con Content-Encoding: br alcanza el decodificador. El nombre no significa que el fallo pertenezca a HTTP/2. Puede activarse mediante HTTP/1.1, una conexión TCP propia, un archivo o cualquier interfaz que descomprima Brotli no confiable. La causa común está en Brotli, no en Express, Node.js ni el protocolo HTTP.
1.1. Conceptos necesarios
- Compression: representación de los mismos datos usando menos bytes. El receptor debe descomprimirlos para recuperar su contenido original.
- Brotli: formato y biblioteca de compresión. En HTTP,
Content-Encoding: brindica que el cuerpo está comprimido con Brotli. - Process: instancia en ejecución de un programa, con su propia memoria y permisos. En este caso puede ser un servidor Node.js, un cliente HTTP o un Worker.
- Buffer: región de memoria reservada para guardar bytes temporalmente. Tiene un comienzo y un tamaño concretos.
- Heap: zona de memoria dinámica donde un proceso reserva buffers, objetos y metadatos mientras trabaja.
- Pointer: valor que señala una dirección de memoria. Si un memory corruption modifica un pointer, el programa puede terminar leyendo, escribiendo o saltando a una dirección elegida de forma indirecta.
- Streaming decompression: procesamiento incremental de fragmentos sin esperar el cuerpo completo. El decodificador conserva un estado entre un fragmento y el siguiente.
- Block: parte lógica de un stream Brotli. Sus metadatos indican el tipo, la longitud y cómo interpretar los datos siguientes.
- Block switch: transición desde un tipo de bloque hacia otro durante la descompresión.
- Ring buffer: buffer circular donde el decodificador conserva bytes recientes. Al alcanzar el final de la zona reservada, vuelve al principio para reutilizarla.
- Out-of-bounds write: operación que escribe más allá de la región reservada para un objeto. Los bytes adicionales pueden sobrescribir otro buffer, un pointer o metadatos del heap.
- ASLR: protección que cambia las direcciones de memoria entre ejecuciones.
- NX: protección que impide ejecutar directamente datos almacenados en regiones no ejecutables.
- ROP: técnica que encadena pequeños fragmentos de instrucciones ya existentes en el binario. Permite realizar operaciones sin ejecutar directamente como código los bytes introducidos por el atacante.
El trust boundary afectado separa el stream comprimido, que puede proceder de Internet, del estado interno del proceso. La operación esperada es sencilla: entran bytes comprimidos y salen bytes descomprimidos. La vulnerabilidad rompe esa separación porque ciertos bits de la entrada terminan modificando memoria que no pertenecía al resultado de la descompresión.
1.2. Mecanismo paso a paso
Brotli puede dividir una secuencia en varias block categories. Para cada categoría, el header declara cuántos block types existen. Si declara uno solo, el tipo actual no puede cambiar: no existe una segunda opción hacia la cual realizar la transición.
- El atacante construye un stream Brotli con una categoría que declara un solo block type.
- El stream fuerza después un block switch imposible para esa categoría.
DecodeBlockTypeAndLength()compruebamax_block_type <= 1. En lenguaje directo, pregunta si existe como máximo un tipo y, por tanto, si el cambio solicitado carece de sentido.- La versión vulnerable devuelve
BROTLI_FALSE, el mismo valor utilizado cuando una lectura incremental necesita más bytes. - En el safe path, el llamador no puede distinguir el error permanente de una pausa temporal. En el fast path, los wrappers del block switch devolvían
voidy descartaban el resultado por completo. - El format error no llega a la salida del decoder. Según el path, el procesamiento continúa o queda aparcado hasta que otro fragmento lo reanuda.
- Al procesar desde ese invalid state, la ruta de copia usa valores corruptos y provoca un out-of-bounds write.
- El efecto depende del objeto alcanzado por el write: puede ocurrir un crash, memory disclosure o la alteración de control data que influya en el control flow.
El fallo decisivo combina dos problemas de state handling: un mismo valor representa «faltan datos» y «formato imposible», y una ruta intermedia no comprueba ese valor. Esperar es correcto cuando faltan bytes, pero el block switch imposible debe terminar la descompresión.
// renderizando diagrama…
Dos tamaños que describen cosas distintas
El aviso de Node.js explica que el decoder seguía añadiendo input a un buffer interno de ocho bytes. La PoC de Rabbitbong identifica después memmove16 como una primitive que realiza un 16-byte out-of-bounds write. Ocho bytes es el tamaño del buffer cuyo límite se pierde; 16 bytes es el tamaño del out-of-bounds write explotado en ese laboratorio.
1.3. La causa visible en el código de Brotli
En el código vulnerable de decode.c, la función devolvía un booleano. Un booleano sólo expresa verdadero o falso, pero el llamador necesitaba distinguir al menos tres resultados: operación completada, entrada incompleta y formato inválido. El caso imposible usaba BROTLI_FALSE, el mismo resultado empleado cuando una lectura incremental todavía no tenía suficientes bits:
static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
int safe, BrotliDecoderState* s, int tree_type) {
brotli_reg_t max_block_type = s->num_block_types[tree_type];
/* ... */
if (max_block_type <= 1) {
return BROTLI_FALSE;
}s->num_block_types[tree_type] contiene la cantidad de tipos declarados para esa categoría. Si el valor es 0 o 1, la condición entra en la rama de error. El problema no está en detectar el caso, sino en comunicarlo mediante un false ambiguo.
La ambigüedad se convierte en memory corruption porque el fast path tampoco conservaba ese false. Por ejemplo, el wrapper de distance blocks devolvía void y no comprobaba el resultado de la función interna:
static void BROTLI_NOINLINE DecodeDistanceBlockSwitch(
BrotliDecoderState* s) {
DecodeDistanceBlockSwitchInternal(0, s);
}Esta capa explica por qué el defecto no es sólo un mensaje de error poco preciso: la operación fallida puede desaparecer de la cadena de llamadas y el bucle continúa con campos que no se actualizaron de forma válida.
El parche oficial de Brotli cambia el tipo de retorno y asigna un error específico al block switch inválido. La versión corregida de decode.c contiene este código:
static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
int safe, BrotliDecoderState* s, int tree_type) {
brotli_reg_t max_block_type = s->num_block_types[tree_type];
/* ... */
if (max_block_type <= 1) {
return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
}BrotliDecoderErrorCode representa éxito, necesidad temporal de más entrada o un error concreto. BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH significa que el stream pidió una transición inválida, de modo que el llamador debe detenerse y rechazar la entrada.
El cambio completo no se limita a reemplazar una constante. Los wrappers de literal, command y distance blocks pasan a devolver BrotliDecoderErrorCode. El nuevo macro BROTLI_SAFE_WITH_STATUS comprueba el resultado tanto en el safe path como en el fast path y lo propaga hasta la salida del decodificador. Así se conserva la diferencia entre una pausa legítima y un formato imposible. Diff completo de decode.c.
2. Superficies alcanzables
Una reachable surface es un path por el que untrusted input llega al componente vulnerable. Tener Brotli vulnerable instalado no confirma que una aplicación pueda ser atacada de forma remota. Esa aplicación debe tomar un stream controlado por otra persona y entregarlo a la función de decompression.
| Surface | Controlled input | Consumer example |
|---|---|---|
| Petición HTTP entrante | Cuerpo con Content-Encoding: br | Express 5 con body-parser 2.x o middleware equivalente que infla el cuerpo |
| Respuesta HTTP consumida | Servidor remoto responde con Content-Encoding: br | Cliente, crawler, webhook, proxy o función fetch() con automatic decompression |
| API directa | Buffer, archivo o mensaje comprimido | node:zlib, bindings nativos u otra biblioteca que invoque Brotli |
| Protocolo propio | Frame comprimido enviado por red | Worker o servicio TCP que descomprime cada registro |
| Documento o artefacto | Archivo que contiene datos Brotli | Importador, conversor o analizador de archivos |
La entrada no siempre es una petición recibida. Un backend que visita una URL indicada por un usuario también consume datos externos: el atacante puede responder con un cuerpo Brotli malformado. Por eso bloquear peticiones Brotli entrantes no protege un crawler, webhook o cliente que descomprime respuestas automáticamente.
2.1. Node.js y Express
Node.js incluye Brotli dentro de node:zlib. Una aplicación puede llamar directamente a sus APIs o usar una capa que lo haga por ella. En Express, esa capa suele ser un middleware: una función que procesa la petición antes de entregarla al controlador final. Express no contiene el memory corruption bug, pero body-parser puede detectar Content-Encoding: br, descomprimir el body y hacer llegar el stream a node:zlib.
El recorrido típico es:
Cliente -> ruta de Express -> body-parser -> node:zlib -> Brotli decoderUna cabecera por sí sola no activa el fallo. Deben cumplirse tres pasos:
- La ruta acepta el método, por ejemplo
POST, y el tipo de cuerpo enviados. - El middleware tiene habilitado
inflate, es decir, decide descomprimir el contenido Brotli. - Los bytes llegan a una copia vulnerable del decoder.
Si un reverse proxy descomprime el body antes de Node.js, Node recibe datos ya expandidos y su copia de Brotli no procesa ese stream. El riesgo puede desplazarse al proxy si éste usa otra copia vulnerable. Para evaluar la exposición hay que identificar qué componente realiza realmente la decompression.
3. Impacto y límites
Primero hay que separar el comportamiento corregido de los posibles efectos del memory corruption. El error -17 hace que el decoder falle de forma segura; una versión vulnerable que supera ese punto puede producir resultados distintos según la memory region afectada:
| Estado o resultado | Qué significa |
|---|---|
| Fail-closed behavior | El decoder corregido devuelve BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH y detiene el stream antes del out-of-bounds write. |
| Crash o DoS | Memory corruption provoca SIGSEGV o termina el process. El servicio deja de responder hasta reiniciarse. |
| Memory disclosure | Una cadena adicional consigue devolver bytes del proceso, que podrían contener direcciones o secretos. |
| RCE | El memory corruption altera control data y permite que el proceso ejecute instrucciones elegidas indirectamente por el atacante. |
Una RCE se ejecuta con la service account, por ejemplo node, no automáticamente como root. Su alcance incluye los archivos, secrets y redes que esa cuenta pueda utilizar.
La RCE tampoco es automática en cada aplicación vulnerable. El out-of-bounds write debe alcanzar un objeto útil, como un pointer, y la cadena debe superar o evitar mitigations como ASLR y NX. El tamaño del ring buffer, el thread donde ocurre la descompresión, el allocator, el binario, las bibliotecas y las allocations realizadas por la aplicación cambian el heap layout, es decir, qué objetos quedan cerca unos de otros.
Un crash demuestra memory corruption o un fallo de manejo para esa entrada y compilación. No demuestra control estable del control flow. Una salida como uid=1000(node) recibida desde el proceso objetivo sí demuestra code execution del binario usado como prueba, con esos permisos.
4. Versiones afectadas y corregidas
4.1. Brotli
El aviso general cubre versiones de Google Brotli desde 1.0.3 hasta 1.2.0. Las primeras versiones corregidas publicadas para las ramas afectadas son: release oficial de Brotli 1.2.1, GitHub Advisory Database.
| Rama afectada | Primera versión corregida |
|---|---|
1.0.3 a 1.0.9 | 1.0.10 |
1.1.0 | 1.1.1 |
1.2.0 | 1.2.1 |
Una distribución puede aplicar el commit 0ed4268a4401abcb44720963 sin cambiar el número principal del paquete. Por eso el changelog y el aviso del proveedor tienen más valor que una comparación aislada de versiones.
Este tipo de corrección se denomina backport: el proveedor toma el parche y lo adapta a un paquete que conserva una versión antigua. Por ese motivo, «la versión parece afectada» sirve para iniciar la revisión, pero no basta para concluir que el paquete carece del parche.
4.2. Node.js
El proyecto Node.js publicó correcciones el 21 de octubre de 2026 para sus tres ramas mantenidas. Aviso oficial, release de Node.js 22.23.4.
| Rama | Primera versión corregida |
|---|---|
| Node.js 22.x | 22.23.4 |
| Node.js 24.x | 24.22.1 |
| Node.js 26.x | 26.12.1 |
Versiones anteriores de Node.js pueden incorporar copias afectadas de Brotli, pero las ramas fuera de soporte no aparecen como objetivos de actualización en el aviso de octubre. Deben migrarse a una rama mantenida o evaluarse mediante el aviso del proveedor que las distribuye.
4.3. Copias embebidas en otros productos
HTTP2Shell es una vulnerabilidad de dependencia. Un producto puede usar Brotli de varias maneras:
- Dynamic library: el ejecutable carga
libbrotliinstalada en el sistema. - Static linking: el código de Brotli queda copiado dentro del ejecutable durante la compilación.
- Vendored copy: el proyecto mantiene su propia copia del código fuente de Brotli y la compila junto con el producto.
Actualizar libbrotli del sistema puede corregir el primer caso, pero no reemplaza automáticamente las statically linked o vendored copies. Esos productos deben reconstruirse o actualizarse por separado.
Por ejemplo, Microsoft publicó actualizaciones específicas para runtimes y aplicaciones autocontenidas de .NET. Su aviso aclara que la exposición requiere descomprimir datos Brotli controlados por una fuente no confiable. Advisory de .NET para CVE-2026-111497.
5. Condiciones necesarias
Conviene separar exposición a memory corruption de reliable RCE. Las primeras condiciones permiten que una entrada remota alcance el defecto. Las últimas describen el trabajo adicional necesario para convertir un out-of-bounds write en code execution.
| Nivel | Condición | Por qué importa |
|---|---|---|
| Memory corruption | Affected decoder | El proceso usa código anterior al parche o una vendored copy sin el backport. |
| Memory corruption | Untrusted input | El atacante controla un stream que la aplicación intenta descomprimir. |
| Memory corruption | Reachable path | Una API, middleware, cliente o parser entrega los bytes al decoder vulnerable. |
| Trigger | Sufficient processing | Los límites de body, tiempo y memoria permiten llegar al block switch imposible. |
| RCE | Useful heap layout | El out-of-bounds write alcanza un pointer, una return address u otro control data que influye en el control flow. |
| RCE | Mitigation bypass | La cadena conoce o evita direcciones aleatorias y utiliza una técnica compatible con las mitigations activas del binario. |
Las tres primeras condiciones bastan para priorizar la actualización por exposición a memory corruption. No hace falta demostrar una RCE contra producción para concluir que el activo necesita el parche. Las condiciones de RCE explican por qué una PoC puede conseguir code execution en una imagen concreta y limitarse a provocar un crash en otra.
6. PoC públicas actualizadas
6.1. Rabbitbong/node2shell: RCE HTTP calibrada para laboratorio
El repositorio Rabbitbong/node2shell publica un laboratorio con Node.js 24.22.0, Express 5 y body-parser 2.x. Su poc.py está restringido a loopback, por lo que sólo acepta como objetivo la propia máquina, y contiene un perfil ROP para el binario x86-64 fijado por el laboratorio. La PoC convierte un 16-byte out-of-bounds write en code execution y usa /usr/bin/id como prueba predeterminada a través de POST /api/v1/import. Esto valida esa imagen y ese heap layout, no cualquier servidor Node.js.
6.1.1. El perfil muestra la dependencia de la compilación
PROFILE contiene posiciones, direcciones de gadgets, el descriptor esperado del socket y el número de intentos:
PROFILE = {
"attempts": 32,
"default_staged_position": 0x137ED748,
"accepted_socket_fd": 19,
"pop_rdi_ret": 0x94FE4D,
"dup2_plt": 0x74D7F0,
"execvp_call_site": 0x1A3EBEC,
}Un gadget es un pequeño grupo de instrucciones que ya existe dentro del ejecutable y termina de una forma útil para ROP. El file descriptor es un número con el que el proceso identifica un recurso abierto; aquí representa el socket de la conexión HTTP. attempts: 32 indica que la PoC reintenta porque el heap layout no siempre coincide a la primera.
Estos valores describen un binario concreto. Si cambia Node.js, la arquitectura, el modo de enlazado o la disposición del ejecutable, las mismas direcciones pueden señalar instrucciones diferentes o memoria no válida. La propia PoC limita el host a 127.0.0.1, ::1 o localhost en resolve_loopback().
La opción --autoprofile puede resolver gadgets y static addresses a partir de otro binario de Node.js, pero no convierte la PoC en universal. Todavía debe acertar el file descriptor del socket y el write target dependiente de ASLR y del heap layout de esa aplicación. Automatizar una parte del profile no elimina esas runtime variables.
6.1.2. Construcción del stream inválido
build_stream() escribe los campos Brotli bit a bit. Estas líneas declaran un único distance block type y preparan el switch imposible:
bits.put(command_block_extra, 24)
varlen_u8(bits, 0) # NBLTYPESD = 1 -> impossible distance switch
bits.put(3, 6) # NPOSTFIX=3, NDIRECT=0NBLTYPESD = 1 significa que sólo existe un distance block type. La cadena genera suficientes commands para agotar el initial block y obligar al decoder a intentar un switch que el formato no permite. En la versión corregida, ese punto produce BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH.
Los comentarios de la PoC son una guía directa: NBLTYPESD cuenta los distance block types y el valor 1 elimina cualquier valid switch target. El input no contiene simplemente «datos raros»; crea de forma deliberada la contradicción que el código vulnerable comunica de manera ambigua.
6.1.3. Segunda entrega y signed displacement
stage_two() crea el segundo fragmento que reanuda el decoder y coloca un signed integer en el campo que la cadena usa como displacement:
body = bytearray(size)
body[7] = 8
struct.pack_into("<i", body, 15, pos)
struct.pack_into("<I", body, 19, 0x7FFFFFFF)
struct.pack_into("<I", body, 23, 0x7FFFFFFF)"<i" escribe pos como signed integer de 32 bits en orden little-endian. El valor sólo resulta útil si el write target calculado cae sobre el dato objetivo de esa ejecución.
En "<i", < selecciona el orden little-endian usado por la plataforma del laboratorio e i indica un signed integer de cuatro bytes. Las dos líneas con "<I" escriben unsigned integers. La PoC controla los bytes, pero eso no equivale a una universal arbitrary write primitive: pos debe coincidir con el memory layout esperado.
6.1.4. Cadena ROP y prueba de ejecución
rce_stages() coloca direcciones del perfil en el prefijo que servirá como fuente de la escritura. El fragmento prepara el ejecutable, duplica el socket aceptado hacia la salida estándar y transfiere el control al sitio de llamada de execvp:
chain = (
# Preparación de la ruta del ejecutable en memoria omitida.
PROFILE["pop_rdi_ret"], socket_fd,
PROFILE["pop_rsi_ret"], 1,
PROFILE["dup2_plt"],
PROFILE["execvp_call_site"],
)En x86-64, rdi y rsi transportan los primeros argumentos de muchas llamadas. Los gadgets pop_rdi_ret y pop_rsi_ret cargan valores controlados en esos registros. Después, dup2 conecta la salida estándar del nuevo programa con el socket HTTP y execvp inicia el ejecutable cuya ruta se guardó en memoria.
La PoC utiliza /usr/bin/id de forma predeterminada, pero --exec permite sustituirlo por la ruta absoluta de otro ejecutable. La cadena sólo controla el filename y no argv, por lo que sigue sin proporcionar una interactive shell o arbitrary command execution. Con la opción predeterminada, la identidad devuelta por la conexión prueba code execution de forma más sólida que un cierre del socket o un HTTP 500. Definición de --exec.
6.1.5. Entrega HTTP en dos escrituras
_build_headers() y send_two_write() construyen una petición HTTP/1.1 con Content-Encoding: br y Transfer-Encoding: chunked. Primero envían el stream grande, esperan y después entregan el fragmento que reanuda el invalid state:
connection.sendall(_HEADERS + chunk(first))
time.sleep(delay)
connection.sendall(chunk(second) + b"0\r\n\r\n")Transfer-Encoding: chunked permite enviar el body en varios chunks sin declarar todo su tamaño por adelantado. La pausa entre ambos writes no es decorativa: separa la creación del invalid state de la entrega que hace continuar al decoder desde ese state. Este código también confirma que el alias HTTP2Shell no implica HTTP/2: el request line de la PoC usa literalmente HTTP/1.1.
attempt_rce() descarta respuestas HTTP ordinarias. Considera prueba el texto crudo devuelto después de que el proceso ejecute el binario seleccionado. Un socket cerrado o un HTTP 500 no se cuentan como RCE.
6.2. Arkango/CVE-2026-111497: separación entre crash y RCE
El repositorio Arkango/CVE-2026-111497 documenta resultados distintos según el consumidor:
| Laboratorio | Entrada | Resultado informado |
|---|---|---|
Express y body-parser sobre Node 24.22.0 | POST Brotli | Crash por SIGSEGV, DoS confirmado |
Worker de transformación sobre Node 24.9.0 | Protocolo TCP con frames | RCE con salida de id |
| Express conectado a Worker | HTTP hacia Worker | Worker crash, sin RCE confirmada |
Un Worker ejecuta trabajo en otro thread con managed memory para esa tarea. PIE permite cargar el executable en direcciones diferentes entre ejecuciones; un non-PIE binary mantiene direcciones más predecibles y simplifica una ROP chain.
La RCE de ese repositorio requiere un binario non-PIE, que el Worker layout quede dentro del rango alcanzable por el signed displacement y un allocation profile específico. El análisis explica que el camino Express ordinario sitúa el ring buffer y el decoder state en memory arenas separadas, lo que dificulta alcanzar con el mismo out-of-bounds write el objeto usado por la cadena. Esta diferencia es importante: una PoC RCE contra un consumidor de Brotli no demuestra RCE universal a través de Express.
6.3. Niveles de evidencia
Al investigar una vulnerabilidad conviene formular la conclusión más estrecha que permite la evidencia. La tabla evita ascender desde «el servidor acepta Brotli» hasta «el servidor permite RCE» sin demostrar los pasos intermedios.
| Resultado | Conclusión válida |
|---|---|
La aplicación anuncia o acepta br | Existe una superficie posible; no confirma la biblioteca ni la CVE. |
| Una entrada Brotli válida se descomprime | La ruta es alcanzable; no distingue versión vulnerable de corregida. |
El trigger produce error de formato -17 | El comportamiento del parche está presente en esa ruta. |
| El process sufre un crash con la entrada malformada | Confirma un fallo para esa cadena y entorno; no confirma RCE. |
| Se filtran bytes ajenos al stream | Confirma memory disclosure en esa reproducción. |
El process devuelve salida fresca de id | Confirma code execution del binario de prueba con los permisos observados. |
| Una PoC funciona en una imagen fijada | Confirma esa combinación; no valida otras compilaciones o aplicaciones. |
7. Validación de exposición
7.1. Inventario del runtime real
En un host Node.js, estos comandos aportan la versión del ejecutable y de la copia Brotli que declara el runtime:
node --version
node -p 'process.versions.brotli'
node -p 'JSON.stringify(process.versions, null, 2)'node --version identifica la versión del runtime. process.versions.brotli muestra la versión de Brotli que Node declara haber incorporado, y el último comando presenta todas las dependencias internas para conservar un inventario más completo.
Hay que ejecutarlos dentro del mismo contenedor, imagen o servicio que procesa las peticiones. El comando node disponible en una terminal puede apuntar a otro ejecutable distinto del usado por systemd, PM2, un contenedor o una función.
La versión informada es evidencia de inventario, no una prueba exhaustiva del parche. Una distribución puede aplicar un backport sin cambiar ese texto y una aplicación puede cargar otra biblioteca por enlace dinámico. La conclusión sólida combina versión, procedencia del paquete o imagen, changelog del proveedor y ejecutable realmente cargado.
7.2. Revisar alcanzabilidad
Conviene buscar:
- Paths que usen Express,
body-parsero middleware con decompression einflatehabilitados. - Llamadas a
createBrotliDecompress,brotliDecompress,BrotliDecodero wrappers equivalentes. - Clientes HTTP, crawlers, webhooks y proxys con automatic response decompression.
- Copias de Brotli dentro de binarios, imágenes, paquetes Python, runtimes .NET, Bun y otros productos.
- Controles del proxy que preservan o eliminan
Content-Encodingantes de llegar a la aplicación.
No es necesario enviar un input que provoque memory corruption para establecer prioridad de parcheo. Una copia afectada más un reachable path que descomprima untrusted input basta para tratar el activo como expuesto.
// renderizando diagrama…
7.3. Detección e investigación
Señales útiles cuando aparecen correlacionadas:
- Peticiones o respuestas con
Content-Encoding: brcerca de un crash. - Cuerpos Brotli grandes con transferencia fragmentada o pausas entre chunks, como los usados por las PoC públicas. El tamaño es un indicio del trigger conocido, no una condición universal de la vulnerabilidad.
SIGSEGV, exit code139, core dumps y reinicios repetidos de Node.js.- Process replacement mediante
execveo unexpected binary execution por la service account. - Outbound connections o lecturas de secrets inmediatamente después de una decompression.
- Varias solicitudes seguidas de crash y recuperación, patrón compatible con intentos probabilísticos contra ASLR.
Un crash aislado puede tener otra causa. La atribución requiere conservar la petición o respuesta comprimida, el core dump, versión y hash del binario, imagen desplegada, process map y eventos del sistema operativo.
El core dump es una copia de la memoria y del process state en el momento del crash. Permite comprobar dónde falló, qué instrucción se ejecutaba y si los bytes de la entrada alcanzaron la zona afectada por el memory corruption. Debe analizarse junto con el binario exacto porque las direcciones cambian entre compilaciones.
8. Mitigación
8.1. Corrección definitiva
- Actualizar Node.js a
22.23.4,24.22.1,26.12.1o una versión posterior mantenida. - Actualizar las bibliotecas Brotli a
1.0.10,1.1.1,1.2.1o al paquete corregido del proveedor. - Reconstruir aplicaciones autocontenidas, imágenes y binarios que incorporen Brotli.
- Reiniciar todos los procesos para retirar de memoria la copia antigua.
- Verificar el ejecutable, imagen, paquete y fecha de inicio realmente desplegados.
Reconstruir es necesario cuando el código vulnerable quedó copiado dentro de una imagen o ejecutable. Reiniciar es necesario porque un proceso que ya estaba en ejecución puede conservar en memoria la biblioteca antigua aunque los archivos del disco se hayan actualizado.
Actualizar únicamente Express o cambiar la ruta no reemplaza la copia vulnerable incluida en Node.js. Actualizar sólo libbrotli del sistema tampoco corrige un binario con Brotli enlazado estáticamente.
8.2. Controles temporales
Si no es posible actualizar de inmediato:
- Deshabilitar Brotli decompression para peticiones entrantes en el proxy y en la aplicación.
- Retirar
brde la automatic decompression de clientes y respuestas salientes. - Evitar que APIs directas procesen streams Brotli de fuentes no confiables.
- Limitar tamaño, tiempo y tasa de cuerpos comprimidos para reducir disponibilidad del vector.
- Aislar el servicio, ejecutar con privilegios mínimos y restringir su acceso a secretos y redes internas.
Bloquear sólo tráfico entrante deja abierta la ruta de respuestas maliciosas. Los límites de tamaño reducen algunos triggers conocidos, pero no corrigen el out-of-bounds write.
9. Clasificación
| Campo | Clasificación |
|---|---|
| Familia | HTTP2Shell / Node2Shell / Broccoli2Shell |
| Identificador | CVE-2026-111497 |
| Debilidad principal | CWE-787: Out-of-bounds Write |
| Componente | Google Brotli C decoder |
| Severidad publicada | Crítica, CVSS v4.0 9.2 |
| 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 |
| Entrada | Stream Brotli controlado por una fuente no confiable |
| Privilegios previos | Ninguno cuando la ruta es remota y pública |
| Interacción de usuario | Ninguna |
| Impacto | Memory corruption, DoS, memory disclosure y RCE potencial |
| Contexto final | Permisos del decompression process |
AT:P expresa que hacen falta condiciones adicionales para alcanzar el impacto completo. La puntuación describe el máximo impacto posible; no afirma que cualquier servicio Brotli permita una RCE fiable con una petición.
Las abreviaturas principales del vector se interpretan así:
| Métrica | Lectura práctica |
|---|---|
AV:N | La entrada puede llegar por red. |
AC:L | El trigger básico no exige una condición excepcionalmente compleja. |
AT:P | El impacto completo depende de condiciones adicionales, como la disposición útil de la memoria. |
PR:N | No requiere una cuenta previa cuando la ruta está expuesta. |
UI:N | No necesita que otra persona abra un archivo o confirme una acción. |
VC:H/VI:H/VA:H | El impacto máximo sobre confidencialidad, integridad y disponibilidad del sistema vulnerable es alto. |
CVSS resume severidad y condiciones generales. No sustituye el análisis de alcanzabilidad de la sección #5. Condiciones necesarias.
10. Referencias y conexiones
- Node.js: October 2026 Security Releases.
- Node.js 22.23.4: release con el backport de Brotli.
- Google Brotli: commit que detecta y propaga el block switch inválido.
- Google Brotli 1.2.1.
- GitHub Advisory Database: CVE-2026-111497.
- NVD: CVE-2026-111497.
- Palo Alto Networks: CVSS y estado de productos para CVE-2026-111497.
- Microsoft .NET advisory para CVE-2026-111497.
- Rabbitbong: laboratorio y PoC RCE HTTP calibrada.
- Rabbitbong: implementación de
poc.py. - Arkango: laboratorios, crash y RCE dependiente del consumidor.
- PHP2Shell, que agrupa CVE-2026-109618 y CVE-2026-109619, fue divulgada la misma semana pero afecta al manejo de memoria y estado de petición dentro de PHP.
- React2Shell, registrada como CVE-2025-55182, es una RCE por deserialización y resolución de objetos en RSC con una causa distinta.
- Denial of Service, porque el process crash es el resultado público más reproducible entre compilaciones.