Un flujo Brotli mal formado, un error que el decodificador confundió con una petición de más datos y ocho bytes de búfer interno que dejaron de ser suficientes. Ese es el mecanismo de Node2Shell, el fallo que desde hace cinco días obliga a reconstruir binarios en media infraestructura web. La escritura fuera de límites vive en el decodificador de Brotli de Google y quedó registrada como CVE-2026-111497, con una puntuación CVSS v4 de 9,2 y explotación remota sin credenciales.
El nombre lo puso el equipo que el 22 de octubre convirtió el error en ejecución de código sobre una imagen oficial de Node.js sin modificar, un día después de que el runtime publicara sus parches. La superficie es más ancha de lo que sugiere el apodo: Brotli 1.0.3 a 1.2.0 viaja dentro de Node.js y dentro de las bindings de Python, dentro de los proxies Envoy e Istio de las mallas de servicio y hasta dentro de Firefox. La telemetría de Akamai sitúa en el 61% las redes que todavía ejecutan versiones afectadas de Node.js.
La parte incómoda llega al parchear. Actualizar la librería del sistema no toca la copia que un runtime compiló dentro de su binario, así que la remediación real exige reiniciar procesos y reconstruir contenedores, ruedas de Python y ejecutables empaquetados. Todo servicio que descomprima cuerpos de petición o respuestas de terceros tiene motivos para empezar por ahí.
El decodificador de Brotli cambia de bloque mientras descomprime, y ese salto depende de una función que en las versiones vulnerables solo devolvía verdadero o falso. El problema es que un falso significaba dos cosas a la vez: que el flujo estaba mal formado o que hacían falta más bytes. Cuando el flujo pedía un cambio de bloque imposible, el decodificador elegía la segunda lectura y seguía trabajando con un estado que ya no era válido.
El bucle de descompresión añadía entonces los bytes entrantes a un búfer interno de ocho posiciones y escribía más allá del final. Desde ese punto la corrupción no afecta al formato, sino a la memoria del proceso que está descomprimiendo.
El arreglo publicado en Brotli 1.2.1 el 21 de octubre hace justo lo contrario que el código vulnerable: devuelve un error propio, `BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH`, con el valor -17, y corta la descompresión en el primer bloque inválido. Node.js aplicó ese mismo parche sobre su copia empotrada en las tres ramas con soporte, y de ahí sale el problema de inventario que se come media tarde de cualquier equipo de plataforma.
¿Quién lleva una copia propia y qué versión la cierra?
Los rangos afectados no coinciden con los números de versión de los productos que los heredan, y esa brecha es la que rompe los inventarios automáticos. Un escáner lee la versión del paquete que aparece en el gestor de dependencias, no la del código nativo que ese paquete compiló dentro.
| Componente |
Versiones afectadas |
Primera versión corregida |
| Brotli (C, upstream) |
1.0.3 a 1.0.9, 1.1.0 y 1.2.0 |
1.0.10, 1.1.1 y 1.2.1 |
| Node.js |
26.x, 24.x y 22.x anteriores al parche, además de las ramas sin soporte |
26.12.1, 24.22.1 y 22.23.4 |
| `brotli` de Python |
hasta 1.2.0 |
1.2.1 |
| `brotlicffi` |
hasta 1.2.0.2 |
1.2.1.0 |
| Bun |
1.0.21 a 1.4.3 |
1.4.4 |
| Envoy |
despliegues con descompresor Brotli activo |
1.40.0, 1.39.3, 1.38.6 o 1.37.8 |
| Istio |
mallas con descompresión Brotli activa |
1.31.2 o 1.30.6 |
Las fechas importan casi tanto como las cifras. Node.js publicó sus tres versiones el 21 de octubre, PyPI recibió `brotli` 1.2.1 y `brotlicffi` 1.2.1.0 al día siguiente y Bun cerró el hueco en la 1.4.4. Quien busque Node2Shell Brotli en los boletines encontrará además los alias HTTP2Shell, Broccoli2Shell y Brotli2Shell, que distintos equipos fueron añadiendo al mismo identificador a medida que comprobaban que el fallo no era cosa de un runtime concreto.
Lo demostrado, lo discutido y lo que no consta
La ejecución de código no es una hipótesis de laboratorio cerrada. El equipo rojo de Imperva logró ejecutar órdenes en la imagen oficial de Node.js 24.22.0 para Linux arm64, tanto en una respuesta que la aplicación descargaba con `fetch()` como en un cuerpo de petición que Express 5 descomprimía por su cuenta. La 24.22.1 rechazó esos mismos datos con un error de formato.
Conviene matizar el alcance. Imperva necesitó varios intentos, no ha publicado su exploit y el PoC público que circula desde el 22 de octubre provoca caídas y lecturas limitadas de memoria en compilaciones estándar.
Red Hat califica el fallo como importante y le asigna un CVSS v3.1 de 8,1, con el argumento de que no existe una prueba pública y fiable de ejecución remota. La denegación de servicio, en cambio, está al alcance de cualquiera que pueda enviar una petición. El aviso original habla de caída del proceso, de lectura de memoria ajena y de ejecución de código arbitrario sin descartar ninguno de los tres.
Nadie ha reportado ataques en producción, y el catálogo de vulnerabilidades explotadas de CISA no incluía el fallo el 24 de octubre. JFrog reprodujo un bloqueo de ClickHouse con un cuerpo de 23 MB enviado con `Content-Encoding: br`, y el arreglo sigue en el repositorio sin release publicado.
Lo que hay que tocar además de la librería del sistema
Descomprimir datos ajenos es la condición para que el fallo importe, y en Node.js esa condición llega por rutas que casi nadie audita. El `fetch()` integrado descomprime respuestas Brotli sin preguntar, de modo que un servidor hostil puede atacar al cliente que lo consulta. Express 5 y body-parser 2 aceptan cuerpos con `Content-Encoding: br` porque la opción `inflate` viene activada de fábrica.
Si no puedes parchear hoy, la salida pasa por cortar la ruta. Un proxy o un WAF que rechace `br` en las peticiones entrantes elimina el vector de entrada, y `express.json({ inflate: false })` lo cierra dentro del propio código. Ninguna de las dos medidas protege la ruta de salida, porque quitar `br` de `Accept-Encoding` no impide que un servidor malicioso envíe Brotli de todos modos.
El inventario es el hueso duro, y el patrón se repite fuera de este fallo. Cuando los navegadores bloquearon ocho intermedias de eMudhra, cada operador tuvo que averiguar por su cuenta qué cadenas dependían de ellas; la empresa india anunció su retirada del negocio global de SSL/TLS el 22 de octubre con la misma lógica de fondo, un componente de confianza del que nadie llevaba la cuenta.
Actualizar el paquete en el portátil no cambia la imagen que se desplegó la semana pasada. Los procesos en marcha siguen con el código viejo hasta que se reinician, y los binarios que empaquetan el runtime por su cuenta, incluidas las compilaciones de Bun, arrastran su propia copia y hay que regenerarlos con la versión corregida.
Dos «Shell» en dos días y la misma lección
El calendario desmiente la idea de que esto sea una racha de mala suerte. php2shell se cerró también el 21 de octubre, el mismo día que Node.js publicó sus parches, y el nombre de Node2Shell apareció al día siguiente. Dos fallos de memoria en dos piezas de infraestructura que ningún equipo inventaría por separado, con la diferencia de que el de PHP vivía en el propio motor y este vive en una librería que varios lenguajes compilan dentro de sus binarios.
Lo que queda para quien administra sistemas no es una lista de CVE. Si el próximo fallo cae en la copia de OpenSSL que un runtime arrastra, el parche del sistema llegará tarde. Un equipo preparado sabe qué versión de Brotli viaja dentro de sus procesos.