Seguridad · Node.js · Gestión de Vulnerabilidades
RCE de Brotli en Node.js: la comprobación no prueba nada
Node.js parcheó un RCE crítico de Brotli el 21 de octubre. Ejecutamos ambos builds oficiales: process.versions.brotli devuelve 1.2.0 en los dos.

El 21 de octubre de 2026, Node.js publicó releases de seguridad por una escritura fuera de límites en el decodificador Brotli que trae incorporado. Datos comprimidos malformados pueden corromper la memoria del proceso y, en el peor caso, ejecutar código del atacante. La base de avisos de GitHub y el NVD le dan 9.2 — Crítica. El aviso del propio Node.js la califica como alta y dice que afecta a 26.x, 24.x y 22.x.
En menos de un día, una instrucción estaba en todas partes: ejecuta node -p "process.versions.brotli" y compara la respuesta con el rango afectado.
La ejecutamos sobre los binarios oficiales. Node 24.22.0, vulnerable, imprime 1.2.0. Node 24.22.1, que ya trae la corrección, imprime 1.2.0. El número que todo el mundo debía leer es idéntico en ambos.
Así que la parte difícil de esta vulnerabilidad no es parchear. El parche es un bump de versión que tu equipo probablemente ya mergeó. La parte difícil es demostrar que llegó a cada runtime que operas — y eso es un problema de inventario, no una consulta al escáner.
Hay una consecuencia que llega antes que cualquier ataque: tu escáner va a marcar en rojo una flota que ya está parcheada, y lo va a hacer indefinidamente. Ese es el costo que casi nadie presupuestó.
Qué verificamos nosotros y qué es alguien hablando de sí mismo
Descargamos node-v24.22.0-linux-x64.tar.xz desde nodejs.org, verificamos su SHA-256 contra el SHASUMS256.txt del propio proyecto y lo ejecutamos junto al 24.22.1. Leímos el version.h incorporado en seis tags de release. Leímos los avisos en la fuente. Esos resultados son nuestros y se reproducen en diez minutos.
Otras afirmaciones muy repetidas en esta divulgación no son nuestras y no están confirmadas de forma independiente:
- Imperva dice haber logrado ejecución remota de código contra una imagen oficial y sin modificar de Node.js 24.22.0. Es la investigación del propio proveedor, el exploit no se publicó, y la propia Imperva afirma que la prueba no funcionó en todos los intentos.
- Vercel e Imperva dicen, cada una, haber desplegado reglas de WAF el día de la divulgación. Cada una es la afirmación de una plataforma sobre sí misma.
- El sitio que concentró la divulgación afirma que Dependabot, pip-audit y Safety no señalaron de forma fiable los paquetes PyPI afectados, y que la prueba de concepto pública solo llega a ejecución de código contra un build debilitado a propósito. Plausible, coherente con la estructura del aviso que sí verificamos, pero no confirmado.
- "Ningún ataque observado en producción" es una ausencia reportada por rastreadores. Nadie puede confirmar una ausencia.
Lee la primera lista como hecho y la segunda como testimonio. El argumento que sigue solo necesita la primera.
El fallo: un valor de retorno que nadie comprobó

En palabras del propio Node.js: un cambio de bloque inválido se reportó como una necesidad temporal de más datos en lugar de un error permanente de formato, así que el decodificador en streaming siguió agregando bytes de entrada a un búfer interno de ocho bytes.
Es una de las formas más viejas del C. Una función devolvía un booleano simple donde hacían falta tres estados — todo bien, necesito más datos, roto de forma permanente. Quien la llamaba leyó "necesito más datos" y siguió alimentando el búfer. Un valor de retorno sin comprobar se convirtió en una escritura fuera de límites.
Y la exposición no se activa a propósito. El fetch() nativo de Node negocia y decodifica respuestas Brotli por su cuenta. Una aplicación queda expuesta con solo llamar a una URL que no controla — una API con la que integras, un webhook, un host de imágenes, cualquier petición que salga de tu servidor.
La cadena de versión no distingue parcheado de roto
Node.js no enlaza contra el Brotli del sistema. Lleva su propia copia de la biblioteca en C dentro de deps/brotli. La corrección de octubre entró en esa copia por cherry-pick, sin subir el número de versión de la biblioteca.

Esa es la release corregida. Las macros de versión dicen 1, 2, 0, y el último commit que tocó el archivo tiene nueve meses.
Esto es lo que reporta la versión incorporada en las tres líneas soportadas. Ejecutamos el par 24.x como binarios y leímos el version.h en las otras cuatro tags:
| Release de Node.js | Trae la corrección | process.versions.brotli |
|---|---|---|
| 22.23.3 | No | 1.1.0 |
| 22.23.4 | Sí | 1.1.0 |
| 24.22.0 | No | 1.2.0 |
| 24.22.1 | Sí | 1.2.0 |
| 26.12.0 | No | 1.2.0 |
| 26.12.1 | Sí | 1.2.0 |
El rango afectado de la biblioteca según el aviso es >= 1.0.3, < 1.2.1, corregido upstream en 1.0.10, 1.1.1 o 1.2.1. Cada fila de esa tabla cae dentro del rango afectado, incluidas las tres corregidas. Una flota Node 22 totalmente parcheada reporta 1.1.0 — una versión que no está corregida en ningún punto upstream y nunca lo estará, porque Node aplicó la corrección sin tomar la release de arriba.
La cadena de versión no es solo inútil. Está equivocada en la dirección que asusta.
La comprobación que sí funciona
El parche agrega un nuevo código de error del decodificador, -17, para el error de formato que antes se reportaba mal. Node lo registra como constante de zlib. Su presencia es, entonces, un marcador fiable de la corrección en el runtime:
node -p "'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('node:zlib').constants"
# false -> este build es vulnerable
# true -> este build tiene la corrección
Lo ejecutamos en ambos binarios oficiales: false en 24.22.0, true en 24.22.1. Hasta donde pudimos comprobar, nadie en la cobertura publicó una prueba de runtime — la guía disponible se detiene en leer la versión de Node.js o cruzar rangos de versión en un inventario que se asume que ya tienes.
Ese one-liner es lo que conviene llevarse de este post. Funciona dentro de un contenedor, en un paso de CI, por SSH en esa máquina que nadie recuerda haber aprovisionado.
Por qué tu escáner no tiene nada que emparejar
Dos razones estructurales, ambas visibles en la propia página del aviso.

Primero: los únicos paquetes legibles por máquina adjuntos a este aviso Crítico son dos de Python. No hay entrada de npm ni entrada de runtime, porque el código vulnerable está compilado dentro del binario de Node, no instalado desde un registro. Un escaneo de dependencias de un proyecto Node.js no tiene nada que reportar. Ningún lockfile de tu repositorio menciona Brotli, y ningún npm audit lo hará.

Segundo: el registro de GitHub no lleva identificador de CVE. El campo dice "No known CVE" mientras las referencias del propio aviso enlazan a la página del NVD de CVE-2026-111497. Consulta la API de avisos de GitHub por esa CVE y recibes un array vacío. Si tu herramienta parte de una CVE hacia la base de GitHub — y muchas lo hacen — esta vulnerabilidad no existe.
Lo que esto cuesta de verdad
Tres partidas de costo, ninguna de ellas curiosidad de ingeniería.
La evidencia del parche se convierte en una auditoría con gente asignada. Cada imagen de contenedor, cada imagen base heredada, runners de CI, runtimes serverless, aplicaciones de ejecutable único, laptops de desarrollo, esa instancia que corre un cron. Ningún manifiesto nombra el código vulnerable, así que la lista tiene que salir de un inventario de runtime. O lo tienes, o lo construyes esta semana, con fecha límite, mientras el cuestionario de seguridad de un cliente espera respuesta.
Tu escáner va a gritar que viene el lobo sobre una flota ya corregida. El emparejamiento de CVE por SBOM lee la versión de la biblioteca incorporada y marca un parque Node 22 totalmente parcheado como Crítico, indefinidamente. Un equipo que aprende a descartar esa alerta rompió la alerta — y la siguiente. Esa es la parte cara, y sobrevive a la vulnerabilidad.
En Node 20, 23 o 25 no hay parche. Esas líneas llegaron al fin de vida antes de la divulgación — 23.x en junio de 2025, 20.x en abril de 2026, 25.x en junio de 2026. Solo 22.x, 24.x y 26.x recibieron la corrección. Para esos equipos, subir de major deja de ser un ítem del roadmap y pasa a ser un incidente, que es una conversación muy distinta sobre calendario y presupuesto.
Esta es la capa que queda sin dueño. Ya escribimos sobre una zona de disponibilidad tumbando a una fila de bancos y sobre contener código que no escribiste; aquí es el mismo fallo en tono más bajo. Los equipos siguen sus dependencias con cuidado y tratan el runtime de abajo como una constante. No es una constante. Es una biblioteca en C que alguien incorporó, con un número de versión que no significa lo que parece.
Puntos clave
process.versions.brotlidevuelve1.2.0tanto en Node 24.22.0 (vulnerable) como en 24.22.1 (corregido). No sirve para verificar tu parche.- Node 22.23.4 está totalmente parcheado y reporta Brotli
1.1.0, dentro del rango afectado del aviso. El emparejamiento por SBOM lo marcará como Crítico para siempre. - La prueba fiable de runtime es la presencia de
BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCHenrequire('node:zlib').constants. - No existe aviso de npm ni entrada en lockfile, y el registro de GitHub no tiene CVE adjunta. El escaneo automático no va a hacer esto por ti.
- Las versiones corregidas son 22.23.4, 24.22.1 y 26.12.1. Node 20, 23 y 25 están en fin de vida, sin parche.
Preguntas frecuentes
¿Estoy expuesto si mi aplicación nunca llama a zlib.brotliDecompress directamente?
Probablemente sí. El fetch() nativo de Node decodifica respuestas Brotli por su cuenta, así que cualquier petición del servidor hacia una URL que no controlas llega al decodificador. Los proxies inversos y el manejo del cuerpo de la petición también.
Mi npm audit está limpio. ¿Significa algo aquí?
No. El código vulnerable está compilado dentro del binario de Node.js y no aparece en ningún manifiesto de paquete. Un audit limpio es el resultado esperado en un runtime vulnerable.
¿Cuál es la forma más rápida de revisar una imagen de contenedor?
Ejecuta la comprobación de la constante contra el intérprete de la propia imagen: docker run --rm --entrypoint node <imagen> -p "'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('node:zlib').constants". Todo lo que imprima false necesita reconstruirse sobre una base parcheada.
Estamos en Node 20. ¿Qué hacemos? Planificar una migración, no un parche. Node 20 llegó al fin de vida el 30 de abril de 2026 y no recibirá esta corrección ni la siguiente. Trátalo como un incidente con fecha.
Llevar a producción software hecho con IA y productos en etapa temprana significa hacerse cargo de las capas que el framework esconde, incluidas las que no tienen manifiesto de paquete. Ese es el trabajo que hacemos.