Diseccionando Node2Shell (CVE-2026-111497): Análisis Técnico de RCE Pre-Autenticación en el Decodificador Brotli de Node.js, Mitigación y Parches

Análisis técnico profundo de Node2Shell (CVE-2026-111497), la vulnerabilidad crítica CVSS 9.2 de escritura fuera de límites en el decodificador Google Brotli integrado en Node.js (c/dec/decode.c), incluyendo causa raíz en C, vectores en Express y fetch(), reglas WAF y guía de actualización.

Diseccionando Node2Shell CVE-2026-111497 RCE en el Decodificador Brotli de Node.js
🚨
Alerta Crítica de Seguridad: Node2Shell (CVE-2026-111497) Identificada como CVE-2026-111497 (CVSS: 9.2 Crítica) y bautizada como Node2Shell, una vulnerabilidad de escritura fuera de límites (Out-of-Bounds Write, CWE-787) en el decodificador nativo Google Brotli (c/dec/decode.c) integrado en el módulo zlib de Node.js permite Ejecución Remota de Código (RCE) sin autenticación mediante peticiones HTTP con Content-Encoding: br o respuestas maliciosas en fetch(). Parcheada en Node.js 26.12.1, 24.22.1 y 22.23.4.

El 21 de octubre de 2026 pasará a la historia como uno de los días más intensos para la seguridad de entornos de ejecución web. El mismo día en que la Fundación PHP lanzó los parches de emergencia para PHP2Shell (CVE-2026-109618 y CVE-2026-109619), el proyecto Node.js publicó actualizaciones críticas para CVE-2026-111497: una vulnerabilidad de corrupción de memoria con puntuación CVSS 9.2 en el motor de descompresión Brotli integrado en Node.js, bautizada como Node2Shell por el equipo de investigación de Imperva.

Muchos desarrolladores de JavaScript asumen que Node.js, al contar con recolección de basura en V8, es inmune a los desbordamientos de búfer clásicos de C y C++. Node2Shell demuestra lo contrario. Dado que Node.js compila y enlaza bibliotecas nativas en C y C++ directamente dentro del binario del intérprete —incluyendo V8, libuv, OpenSSL, llhttp y la implementación oficial en C de Google Brotli dentro del módulo nativo node:zlib—, un fallo de memoria en cualquiera de esas dependencias se ejecuta en el mismo espacio de direcciones que tu servidor Express, Fastify o Next.js.

Peor aún: tanto los middlewares modernos (como body-parser en Express 5) como el cliente nativo fetch() descomprimen automáticamente los flujos Brotli antes de que tu código JavaScript llegue siquiera a inspeccionarlos. En este análisis técnico examinamos la causa raíz en C dentro de c/dec/decode.c, los vectores de ataque entrantes y salientes, y las guías de parcheo y mitigación con WAF.


1. Superficie de Ataque Bidireccional: Cómo se Activa CVE-2026-111497

No necesitas importar node:zlib manualmente en tu aplicación para ser vulnerable. Un servicio estándar en Node.js queda expuesto a través de dos vectores HTTP automáticos:

VECTOR 1: Cuerpo de Petición HTTP Entrante (RCE Pre-Autenticación en Servidor)
  Atacante ──[ POST /api/login | Content-Encoding: br ]──► Express 5 / body-parser (2.0.0–2.3.0)
                                                                 │
                                                    zlib.createBrotliDecompress()
                                                                 │
                                                    deps/brotli/c/dec/decode.c
                                                                 │
                                                    💥 Escritura Fuera de Límites (CWE-787) -> RCE

VECTOR 2: Petición HTTP Saliente con fetch() (RCE vía Cliente / SSRF / Webhooks)
  App Node.js ──[ await fetch(urlExterna) ]──► Servidor Malicioso responde Content-Encoding: br
                                                                 │
                                                    Descompresión automática Brotli en Undici
                                                                 │
                                                    💥 Escritura Fuera de Límites (CWE-787) -> RCE

Vector 1: Descompresión de Peticiones Entrantes (Express 5 y body-parser 2.x)

Cuando configuras app.use(express.json()) o app.use(express.urlencoded()), el middleware body-parser tiene activada por defecto la opción inflate: true. Desde las versiones 2.0.0 a 2.3.0 de body-parser (incluidas en Express 5.0.0–5.3.0), el middleware descomprime automáticamente cualquier petición con encabezado Content-Encoding: br invocando zlib.createBrotliDecompress().

Dado que el análisis del cuerpo se ejecuta antes que los controladores de ruta, los validadores de esquema (Zod/Joi) o los middlewares de autenticación por ruta, un atacante no autenticado puede enviar una sola petición POST con Content-Encoding: br para disparar la corrupción de memoria en C antes de que se ejecute tu lógica de negocio.

Vector 2: Peticiones Salientes con fetch()

Si tu servidor Node.js realiza peticiones HTTP hacia URLs proporcionadas por usuarios o servicios externos (webhooks, vistas previas de enlaces OpenGraph, descubrimiento OIDC, lectores RSS o agentes de scraping), el cliente nativo fetch() envía automáticamente Accept-Encoding: gzip, deflate, br y descomprime de forma transparente cualquier respuesta con Content-Encoding: br utilizando el mismo decodificador vulnerable.


2. Causa Raíz en Código C: Diseccionando c/dec/decode.c y el Commit 0ed4268

CVE-2026-111497 reside en la biblioteca oficial de Google Brotli (versiones 1.0.3 a 1.2.0), específicamente en la máquina de estados de descompresión dentro de c/dec/decode.c.

Cómo Funciona el Cambio de Tipo de Bloque en Brotli

Bajo el estándar RFC 7932, cada meta-bloque comprimido en Brotli divide los símbolos en tres categorías de bloques: literales (categoría 0), comandos de inserción/copia (categoría 1) y distancias (categoría 2). Para gestionar los cambios de árboles Huffman a mitad del flujo sin alojar memoria constantemente, la estructura en C BrotliDecoderStateStruct (c/dec/state.h) mantiene arreglos de estado, ringbuffers (block_type_rb) y punteros a funciones de gestión de memoria directamente en el heap:

/* Estructura simplificada de BrotliDecoderStateStruct (c/dec/state.h) */
struct BrotliDecoderStateStruct {
    BrotliRunningState state;
    int loop_counter;
    const uint8_t* pos;
    /* ... */
    uint8_t* ringbuffer;
    uint8_t* ringbuffer_end;
    HuffmanCode* htree_command;
    uint32_t block_type_rb[6];     /* Historial por categoría de bloque (0, 1, 2) */
    uint32_t num_block_types[3];   /* Cantidad de tipos de bloque declarados */
    HuffmanCode* block_type_trees;
    HuffmanCode* block_len_trees;
    brotli_alloc_func alloc_func;  /* ⚠️ Puntero a función de alojamiento en el struct */
    brotli_free_func  free_func;   /* ⚠️ Puntero a función de liberación en el struct */
    void* memory_manager_opaque;
    /* ... */
};
🔬
La Primitiva de Escritura Fuera de Límites (Out-of-Bounds Write, CWE-787) Cuando un flujo de bits Brotli manipulado codifica una secuencia inválida de cambio de tipo de bloque en c/dec/decode.c, el decodificador sin parchear no valida la precondición a tiempo. Como resultado, calcula un índice fuera de rango y escribe más allá del búfer de estado, sobrescribiendo campos adyacentes dentro de BrotliDecoderStateStruct o estructuras vecinas en el heap de C++/Node.js.

Al sobrescribir punteros adyacentes dentro de la estructura —como el puntero de liberación free_func (que se invoca automáticamente al destruir el flujo con BrotliDecoderDestroyInstance()), los punteros del ringbuffer o las tablas de métodos virtuales (vtable) de C++ en node::zlib::BrotliDecoder—, el atacante obtiene control de ejecución de código nativo, tal como demostró el Red Team de Imperva sobre contenedores oficiales de Node.js (node:24.22.0 en Linux).

El parche oficial en el commit 0ed4268 de Google Brotli ("Detect and report invalid stream earlier") introduce una validación temprana en c/dec/decode.c que aborta inmediatamente el procesamiento con el código de error BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH = -17 antes de realizar cualquier escritura fuera de límites.


3. Matriz de Versiones Afectadas y Parcheadas

Entorno / Biblioteca Línea de Soporte Versiones Vulnerables Versión Parcheada
Node.js 26.x Versión Actual < 26.12.1 v26.12.1+
Node.js 24.x LTS Activo < 24.22.1 v24.22.1+
Node.js 22.x LTS de Mantenimiento < 22.23.4 v22.23.4+
Node.js 20.x / 18.x / 23.x / 25.x Fin de Vida (EOL) Todas las versiones con Brotli 1.0.3–1.2.0 Sin parche oficial (Migrar a 22 / 24 / 26)
Google Brotli (Biblioteca C) Upstream libbrotlidec 1.0.3 – 1.2.0 1.0.10 / 1.1.1 / 1.2.1+ (Commit 0ed4268)

Puedes verificar qué versión de Node.js y Brotli tienes compilada ejecutando:

node -p " ({ node: process.version, brotli: process.versions.brotli }) "

4. Mitigación en Profundidad y Parcheo Virtual (Express, Nginx y WAF)

A. Bloquear Content-Encoding: br en Express / body-parser

Los navegadores web estándar no comprimen las peticiones salientes POST con Brotli (Content-Encoding: br se usa casi exclusivamente en respuestas del servidor al navegador). Puedes rechazar el encabezado antes de express.json() o desactivar inflate:

// Opción 1: Rechazar Content-Encoding: br ANTES de express.json() / body-parser
app.use((req, res, next) => {
  const encoding = (req.headers['content-encoding'] || '').toLowerCase();
  if (encoding.includes('br')) {
    return res.status(415).json({ error: 'Codificacion Brotli en peticiones no soportada' });
  }
  next();
});

// Opción 2: O desactivar la descompresión automática en express.json()
app.use(express.json({ inflate: false }));
app.use(express.urlencoded({ extended: true, inflate: false }));

B. Proteger Llamadas Salientes fetch() Hacia Servidores No Confiables

const response = await fetch(urlWebhookNoConfiable, {
  headers: {
    'Accept-Encoding': 'gzip, deflate' // Omitir explícitamente 'br'
  }
});

C. Reglas en Nginx y WAF (ModSecurity / Coraza / Centinela)

1. En Proxy Inverso Nginx:

# Rechazar peticiones entrantes comprimidas con Brotli
if ($http_content_encoding ~* "\bbr\b") {
    return 415;
}

2. Regla ModSecurity / Coraza:

SecRule REQUEST_HEADERS:Content-Encoding "(?i)\bbr\b" \
    "id:1011497,phase:1,deny,status:415,log,t:none,msg:'Node2Shell (CVE-2026-111497) Peticion Brotli Entrante Bloqueada'"

5. Caza de Amenazas (Threat Hunting) y Auditoría de Caídas

Al tratarse de un desbordamiento de búfer en C dentro del proceso de Node.js, los intentos de explotación suelen provocar caídas abruptas de los contenedores o servicios con señales SIGSEGV (código de salida 139) o SIGABRT (código 134):

# Auditar logs de systemd en busca de violaciones de segmento en procesos node
journalctl -u mi-servicio-node --since "7 days ago" | grep -E "SIGSEGV|SIGABRT|corrupted|invalid pointer|core dumped"

# Buscar pods de Kubernetes reiniciados con código 139 (SIGSEGV) o 134 (SIGABRT)
kubectl get pods -A -o json | jq -r '.items[] | select(.status.containerStatuses[]?.lastState.terminated.exitCode == 139 or .status.containerStatuses[]?.lastState.terminated.exitCode == 134) | .metadata.name'

Conclusión

Node2Shell (CVE-2026-111497) es un recordatorio contundente de que las bibliotecas en C integradas bajo las APIs de alto nivel de JavaScript pueden introducir riesgos críticos de corrupción de memoria pre-autenticación. Actualiza tus entornos Node.js a las versiones v26.12.1, v24.22.1 o v22.23.4 de inmediato, bloquea los encabezados Content-Encoding: br entrantes no solicitados en tu proxy inverso o WAF, y planifica la migración de cualquier carga de trabajo que aún dependa de Node 18 o Node 20.

Read more