cd ~/blog

~/vulns/http2shell-node2shell-brotli-rce-cve-2026-111497.md

HTTP2Shell - Node2Shell Brotli RCE (CVE-2026-111497)

webautor: Jordy Pérez Osorio aviso legal
webNodejsBrotliHTTP2ShellNode2ShellRCEmemory-corruptionout-of-bounds-write

~23 min de lectura


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: br indica 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.

  1. El atacante construye un stream Brotli con una categoría que declara un solo block type.
  2. El stream fuerza después un block switch imposible para esa categoría.
  3. DecodeBlockTypeAndLength() comprueba max_block_type <= 1. En lenguaje directo, pregunta si existe como máximo un tipo y, por tanto, si el cambio solicitado carece de sentido.
  4. La versión vulnerable devuelve BROTLI_FALSE, el mismo valor utilizado cuando una lectura incremental necesita más bytes.
  5. 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 void y descartaban el resultado por completo.
  6. 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.
  7. Al procesar desde ese invalid state, la ruta de copia usa valores corruptos y provoca un out-of-bounds write.
  8. 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.

SurfaceControlled inputConsumer example
Petición HTTP entranteCuerpo con Content-Encoding: brExpress 5 con body-parser 2.x o middleware equivalente que infla el cuerpo
Respuesta HTTP consumidaServidor remoto responde con Content-Encoding: brCliente, crawler, webhook, proxy o función fetch() con automatic decompression
API directaBuffer, archivo o mensaje comprimidonode:zlib, bindings nativos u otra biblioteca que invoque Brotli
Protocolo propioFrame comprimido enviado por redWorker o servicio TCP que descomprime cada registro
Documento o artefactoArchivo que contiene datos BrotliImportador, 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 decoder

Una cabecera por sí sola no activa el fallo. Deben cumplirse tres pasos:

  1. La ruta acepta el método, por ejemplo POST, y el tipo de cuerpo enviados.
  2. El middleware tiene habilitado inflate, es decir, decide descomprimir el contenido Brotli.
  3. 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 resultadoQué significa
Fail-closed behaviorEl decoder corregido devuelve BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH y detiene el stream antes del out-of-bounds write.
Crash o DoSMemory corruption provoca SIGSEGV o termina el process. El servicio deja de responder hasta reiniciarse.
Memory disclosureUna cadena adicional consigue devolver bytes del proceso, que podrían contener direcciones o secretos.
RCEEl 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 afectadaPrimera versión corregida
1.0.3 a 1.0.91.0.10
1.1.01.1.1
1.2.01.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.

RamaPrimera versión corregida
Node.js 22.x22.23.4
Node.js 24.x24.22.1
Node.js 26.x26.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 libbrotli instalada 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.

NivelCondiciónPor qué importa
Memory corruptionAffected decoderEl proceso usa código anterior al parche o una vendored copy sin el backport.
Memory corruptionUntrusted inputEl atacante controla un stream que la aplicación intenta descomprimir.
Memory corruptionReachable pathUna API, middleware, cliente o parser entrega los bytes al decoder vulnerable.
TriggerSufficient processingLos límites de body, tiempo y memoria permiten llegar al block switch imposible.
RCEUseful heap layoutEl out-of-bounds write alcanza un pointer, una return address u otro control data que influye en el control flow.
RCEMitigation bypassLa 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=0

NBLTYPESD = 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:

LaboratorioEntradaResultado informado
Express y body-parser sobre Node 24.22.0POST BrotliCrash por SIGSEGV, DoS confirmado
Worker de transformación sobre Node 24.9.0Protocolo TCP con framesRCE con salida de id
Express conectado a WorkerHTTP hacia WorkerWorker 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.

ResultadoConclusión válida
La aplicación anuncia o acepta brExiste una superficie posible; no confirma la biblioteca ni la CVE.
Una entrada Brotli válida se descomprimeLa ruta es alcanzable; no distingue versión vulnerable de corregida.
El trigger produce error de formato -17El comportamiento del parche está presente en esa ruta.
El process sufre un crash con la entrada malformadaConfirma un fallo para esa cadena y entorno; no confirma RCE.
Se filtran bytes ajenos al streamConfirma memory disclosure en esa reproducción.
El process devuelve salida fresca de idConfirma code execution del binario de prueba con los permisos observados.
Una PoC funciona en una imagen fijadaConfirma 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-parser o middleware con decompression e inflate habilitados.
  • Llamadas a createBrotliDecompress, brotliDecompress, BrotliDecoder o 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-Encoding antes 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: br cerca 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 code 139, core dumps y reinicios repetidos de Node.js.
  • Process replacement mediante execve o 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

  1. Actualizar Node.js a 22.23.4, 24.22.1, 26.12.1 o una versión posterior mantenida.
  2. Actualizar las bibliotecas Brotli a 1.0.10, 1.1.1, 1.2.1 o al paquete corregido del proveedor.
  3. Reconstruir aplicaciones autocontenidas, imágenes y binarios que incorporen Brotli.
  4. Reiniciar todos los procesos para retirar de memoria la copia antigua.
  5. 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 br de 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

CampoClasificación
FamiliaHTTP2Shell / Node2Shell / Broccoli2Shell
IdentificadorCVE-2026-111497
Debilidad principalCWE-787: Out-of-bounds Write
ComponenteGoogle Brotli C decoder
Severidad publicadaCrítica, CVSS v4.0 9.2
VectorCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
EntradaStream Brotli controlado por una fuente no confiable
Privilegios previosNinguno cuando la ruta es remota y pública
Interacción de usuarioNinguna
ImpactoMemory corruption, DoS, memory disclosure y RCE potencial
Contexto finalPermisos 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étricaLectura práctica
AV:NLa entrada puede llegar por red.
AC:LEl trigger básico no exige una condición excepcionalmente compleja.
AT:PEl impacto completo depende de condiciones adicionales, como la disposición útil de la memoria.
PR:NNo requiere una cuenta previa cuando la ruta está expuesta.
UI:NNo necesita que otra persona abra un archivo o confirme una acción.
VC:H/VI:H/VA:HEl 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

// relacionados