Envío de registros, alertas y datos de telemetría a través de un diodo de datos

Descubre cómo
Utilizamos inteligencia artificial para traducir el sitio web y, aunque nos esforzamos por garantizar la precisión, es posible que las traducciones no sean siempre 100 % exactas. Agradecemos tu comprensión.

React2Shell (CVE-2025-55182): Vulnerabilidad crítica de ejecución remota de código en Server de React

Por Loc Nguyen, jefe del equipo de pruebas de penetración
Comparte esta publicación

CVE-2025-55182 es una vulnerabilidad crítica de ejecución remota de código previa a la autenticación en React Server , con una puntuación CVSS de 10,0, la máxima calificación de gravedad posible. Como parte del Programa de BecasOPSWAT , nuestros becarios llevaron a cabo un análisis técnico exhaustivo de esta vulnerabilidad, examinando su causa raíz en el protocolo de deserialización de React Flight, la cadena de explotación completa y su amplio impacto en el ecosistema web moderno. En este blog presentamos nuestros hallazgos junto con recomendaciones prácticas para los defensores.

React se ha convertido en una de las bibliotecas front-end más utilizadas del mundo, y es la base de una parte significativa de mobile web y mobile modernas. Las encuestas a desarrolladores de Stack Overflow sitúan sistemáticamente a React entre los principales marcos web, con una adopción que supera el 40 % de los desarrolladores profesionales a nivel mundial. Paralelamente a este crecimiento, el equipo de React introdujo Server React Server (RSC) como una característica fundamental de React 19: un cambio de paradigma que traslada la lógica de renderizado del cliente al servidor, lo que permite un rendimiento optimizado y una integración más estrecha entre el código del lado del servidor y el del lado del cliente.

Sin embargo, esta evolución arquitectónica introdujo una nueva superficie de ataque crítica. El 29 de noviembre de 2025, el investigador de seguridad Lachlan Davidson notificó una vulnerabilidad en la lógica de deserialización del lado del servidor de React al programa Bug Bounty de Meta. La vulnerabilidad, que se hizo pública el 3 de diciembre de 2025 con el identificador CVE-2025-55182, permite la ejecución remota de código sin autenticación mediante una única solicitud HTTP manipulada. Clasificada como CWE-502 (Deserialización de datos no confiables), la vulnerabilidad no requiere autenticación, interacción del usuario ni configuración especial de la aplicación: una implementación predeterminada de «create-next-app» creada para producción es inmediatamente explotable.

Figura 1: CVE-2025-55182 (fuente: NVD)

El impacto fue inmediato y grave. En las 48 horas siguientes a la divulgación pública, se observaron múltiples campañas de explotación en el entorno real. Según la Fundación Shadowserver, se identificaron más de 77 000 direcciones IP públicas como potencialmente vulnerables. La telemetría de Cloudflare registró más de 582 millones de intentos de explotación en la semana posterior a la divulgación, con una intensidad de ataque que superó una media de 3.500 direcciones IP de origen únicas por hora y alcanzó un pico de 16.585 direcciones IP atacantes simultáneas. Wiz Research informó de que el 39 % de los entornos en la nube contenían instancias vulnerables. La CISA añadió la vulnerabilidad a su catálogo de Vulnerabilidades Explotadas Conocidas (KEV) el 5 de diciembre de 2025.

Los autores de las amenazas actuaron con notable rapidez y diversidad. Trend Micro documentó múltiples campañas —entre ellas las campañas de las redes de bots «emerald» y «nuts»— en las que se utilizaron balizas de Cobalt Strike, implantes de Sliver, el agente de monitorización Nezha, túneles Fast Reverse Proxy (FRP) y una novedosa carga útil denominada «Secret-Hunter», que aprovecha herramientas de código abierto para la recolección de credenciales, como TruffleHog y Gitleaks. Threat Intelligence de Google identificó distintos clústeres de amenazas vinculados a China (UNC6600, UNC6586, UNC6588, UNC6603) que desplegaban herramientas especializadas —entre ellas el tunelizador MINOCAT, el descargador SNOWLIGHT, la puerta trasera COMPOOD y la puerta trasera HISONIC— junto con actores vinculados a Irán y grupos con motivaciones económicas que llevaban a cabo campañas de minería de criptomonedas. AWS documentó que grupos vinculados a China estaban experimentando con código de explotación ya el 4 de diciembre, antes de que el código de prueba de concepto completo estuviera disponible públicamente.

Acerca de Server de React

React es una biblioteca de JavaScript para crear interfaces de usuario, mantenida por Meta y una amplia comunidad de código abierto. Server de React (RSC), introducidos con React 19, representan un cambio fundamental en la forma en que las aplicaciones de React gestionan la representación. A diferencia de los componentes de cliente tradicionales, que se ejecutan íntegramente en el navegador, los componentes de servidor se ejecutan en el servidor, generando una representación serializada de la interfaz de usuario que se transmite al cliente. Este diseño reduce la cantidad de JavaScript enviada al navegador, mejora las métricas de tiempo de interactividad y permite el acceso directo a recursos del lado del servidor, como bases de datos y sistemas de archivos.

Figura 2: React Server (RSC)

RSC se basa en un protocolo de serialización personalizado denominado «Flight» para codificar y transmitir datos entre el cliente y el servidor. Cuando un cliente invoca una Server (antes conocida como Server ), el navegador empaqueta los argumentos de la función en una solicitud HTTP estructurada utilizando el formato Flight. El servidor deserializa esta carga útil, ejecuta la función solicitada y envía el resultado al cliente. Este estrecho acoplamiento entre el cliente y el servidor (aunque arquitectónicamente elegante) implica que cualquier fallo en la lógica de deserialización puede tener consecuencias inmediatas y catastróficas, como demuestra CVE-2025-55182.

La vulnerabilidad no solo afecta a React en sí, sino a todo el ecosistema de marcos de trabajo desarrollados sobre él. Next.js (que recibió un aviso independiente, CVE-2025-66478, posteriormente rechazado por ser un duplicado), React Router, Waku, el complemento RSC de Parcel, el complemento RSC de Vite y RedwoodSDK se ven todos afectados. Incluso las aplicaciones que no definen explícitamente Server pueden ser vulnerables si el soporte para RSC está habilitado en el marco de trabajo.

Antecedentes técnicos

Antes de analizar la vulnerabilidad, hay tres conceptos fundamentales que sustentan la cadena de explotación: el comportamiento de la instrucción «await» de JavaScript con objetos «thenable», el recorrido de la cadena de prototipos y el modelo de datos basado en fragmentos del protocolo React Flight.

«await» y los objetos «Thenable» en JavaScript

El operador `await` detiene la ejecución de una función asíncrona hasta que se resuelve la expresión esperada. Cuando `await` encuentra una `Promise` nativa, espera a que se resuelva y devuelve el valor resultante. Sin embargo, `await` no requiere una `Promise` nativa: cualquier objeto que cuente con un método `.then()`, conocido como «thenable», se trata como una construcción similar a una promesa.

Cuando «await» encuentra un objeto «thenable», invoca el método .then() de dicho objeto, pasando las funciones de llamada «resolve» y «reject» proporcionadas por el sistema. El valor pasado a «resolve» se convierte en el resultado de la expresión «await». Es fundamental señalar que, si el valor resuelto es a su vez un thenable, el método .then() de ese objeto anidado se invoca de forma recursiva hasta que se alcanza un valor primitivo o una Promise resuelta. Este comportamiento de resolución recursiva es fundamental para la explotación de CVE-2025-55182.

Recorrido por la cadena de prototipos

Cada objeto de JavaScript mantiene un enlace interno a su prototipo, al que se puede acceder a través de la propiedad __proto__. Cuando se accede a una propiedad de un objeto, el motor de JavaScript comprueba primero las propiedades propias del objeto. Si no encuentra la propiedad, el motor recorre la cadena de prototipos —siguiendo cada enlace __proto__ hacia arriba— hasta que encuentra la propiedad o la cadena termina en «undefined».

Un atacante puede aprovechar este mecanismo de herencia para acceder a propiedades que se encuentran fuera del ámbito previsto de un objeto. Al incluir __proto__ en las rutas de acceso a las propiedades, un atacante puede acceder a métodos y constructores internos que la aplicación nunca tuvo la intención de exponer. En JavaScript, la expresión obj.__proto__.constructor.constructor devuelve el constructor global Function, que puede crear y ejecutar funciones arbitrarias a partir de una cadena de entrada.

El protocolo React Flight y el modelo de datos por fragmentos

When a client invokes a Server Function, the browser sends an HTTP POST request with a multipart/form-data body. Each form field contains a numbered “chunk” of serialized data. The Flight protocol uses special string prefixes to encode data types: $<id> references the resolved value of another chunk, $@<id> references the raw chunk object itself, $W<id> represents a Set, $K<id> represents FormData, and $B<id> triggers the blob handler.

Consideremos una Server definida de la siguiente manera:

Figura 3: Ejemplo de Server

La solicitud HTTP correspondiente contiene varios campos de formulario, cada uno de los cuales consta de una clave y un valor: el campo 0 contiene la matriz de argumentos con referencias como «$W1» y «$K2», mientras que los campos 1 y 2_* contienen los datos a los que remiten dichas referencias. El servidor procesa cada campo a medida que lo recibe, almacenando los resultados intermedios en objetos denominados «chunks».

Figura 4: Solicitud HTTP «multipart/form-data» generada al invocar la Server del ejemplo

Un chunk es un objeto «thenable» con cuatro propiedades clave: status (el estado de resolución), value (los datos almacenados), reason (información sobre el error) y _response (una referencia al objeto de respuesta principal). Cuando el servidor encuentra await chunk, se invoca el método .then() del chunk. Si el estado del chunk es INITIALIZED, la llamada de retorno de resolución recibe chunk.value. Si el estado es PENDING, BLOCKED o CYCLIC, las llamadas de retorno se ponen en cola para su ejecución posterior.

Figura 5: Estado del objeto «chunk» durante la deserialización

El chunk 0 suele representar la matriz de argumentos de la Server invocada. Una vez recibidos todos los campos del formulario y resueltas todas las referencias internas, chunk_0.value contiene la matriz de argumentos completamente montada, que a continuación se pasa a la función de destino.

Gestión de solicitudes de extremo a extremo (Next.js → Deserialización de React Flight)

A continuación se describe cómo procesa Next.js una solicitud entrante Server en condiciones normales, desde la capa HTTP hasta el motor de deserialización de React Flight.

Figura 6: Descripción general del procesamiento de solicitudes en Server de Next.js

Función handleAction() - Next.js

Cuando se invoca una Server , la solicitud entra en la función `handleAction`. Esta función valida los metadatos, comprueba los encabezados y los tokens CSRF, y confirma que la solicitud es una acción de recuperación válida. A continuación, se crea un flujo denominado busboyStream para analizar el cuerpo del formulario multiparte. La función decodeReplyFromBusboy vincula emisores de eventos a este flujo, lo que activa las funciones de controlador de deserialización ServerReact Servera medida que se reciben los datos sin procesar. El valor de retorno de decodeReplyFromBusboy es chunk_0; el operador await lo resuelve y pasa su valor ensamblado a la Server invocada.

Figura 7: función handleAction

La función `getChunk` devuelve el fragmento correspondiente a un ID determinado. Si ese fragmento aún no existe, crea un ` ResolvedModelChunk ` (si ya hay datos en `response._formData`) o un `PendingChunk` (si aún no han llegado datos para ese ID).

Figura 8: Función getChunk

Cuando decodeReplyFromBusboy devuelve chunk_0, el fragmento sigue en estado PENDING. El operador await llama a chunk_0.then() y almacena temporalmente las funciones de llamada de retorno resolve y reject en chunk_0.value y chunk_0.reason. Estas funciones de llamada de retorno son reactivadas por la función wakeChunk una vez que se completa la resolución de referencias.

Figura 9: Función wakeChunk

Proceso de deserialización - React Server

Cuando busboyStream recibe un campo de datos sin procesar completo, activa el emisor de eventos «field», invoca la función resolveField e inicia la deserialización, es decir, la conversión de los datos sin procesar del formulario en objetos JavaScript completamente construidos. Las siguientes funciones controlan este proceso.

resolveField(respuesta, clave, valor)

Figura 10: Función `resolveField`

La clave y el valor se añaden a `response._formData`. A continuación, la función recupera el fragmento correspondiente al ID que coincide con la clave. Si ese fragmento ya existe, se llama a `resolveModelChunk` para reconstruirlo. Esta resolución diferida es necesaria porque un valor puede contener referencias a campos cuyos datos sin procesar aún no han llegado; en ese caso, React Server un `PendingChunk` con callbacks personalizados de resolución y rechazo para gestionar esas referencias más adelante.

resolveModelChunk(chunk, valor, id)

Figura 11. Función `resolveModelchunk`

resolveModelChunk crea un ResolvedModelChunk con el estado RESOLVED_MODEL e inyecta los datos sin procesar. A continuación, reconstruye el fragmento mediante initializeModelChunk y llama a wakeChunk para activar cualquier callback de resolución o rechazo en cola, completando así la resolución del objeto o la referencia.

initializeModelChunk(chunk)

Figura 12: función initializeModelChunk

initializeModelChunk cambia el estado del fragmento a CYCLIC —lo que indica que se está llevando a cabo la resolución de referencias— y comienza la deserialización. Crea un objeto JavaScript sin procesar a partir de chunk.value utilizando JSON.parse y, a continuación, pasa este objeto a la función reviveModel.

reviveModel(respuesta, objetoPadre, clavePadre, valor, referencia)

Figura 13: Función reviveModel

reviveModel procesa de forma recursiva cada componente del objeto analizado. Cuando encuentra un valor de cadena, llama a parseModelString para gestionarlo.

parseModelString(respuesta, objeto, clave, valor, referencia)

Figura 14. Función parseModelString

parseModelString se activa en función del prefijo de la cadena para gestionar los distintos tipos de codificación. En el caso de las referencias que comienzan por $, se llama a la función getOutlinedModel para resolver la referencia entre fragmentos.

getOutlinedModel(respuesta, referencia, objetoPadre, clave, mapa)

Figura 15: Función getOutlinedModel

getOutlinedModel divide la referencia por el delimitador «:» para formar una ruta de acceso a la propiedad y, a continuación, recorre esa ruta en el objeto fragmento de destino para devolver el valor al que se hace referencia. Tal y como se detalla en el análisis de vulnerabilidades que figura a continuación, la falta de validación de estos nombres de propiedades es precisamente el punto en el que reside la vulnerabilidad.

Análisis de vulnerabilidades

Causa principal

CVE-2025-55182 originates from insufficient input validation in the getOutlinedModel() function within React’s server-side Flight reply handler (ReactFlightReplyServer.js). When a chunk reference includes a property path - such as $<id>:<prop1>:<prop2> - the function resolves it by traversing the specified properties on the target chunk object, computing the result as chunk[prop1][prop2].

Figura 16: Validación insuficiente de los datos de entrada en la función getOutlinedModel()

El fallo crítico radica en que estos nombres de propiedades nunca se validan. El servidor no comprueba si las propiedades solicitadas son propiedades propias del objeto o propiedades del prototipo heredadas. Por lo tanto, un atacante puede incluir __proto__ en la ruta de la propiedad para recorrer la cadena de prototipos y acceder a métodos internos a los que nunca se debería poder acceder desde entradas controladas por el usuario. Por ejemplo, la referencia $1:__proto__:then se resuelve como Chunk.prototype.then, una función que el atacante puede invocar con argumentos controlados.

Figura 17: Recorrido del prototipo mediante entradas maliciosas

Código vulnerable

La cadena de explotación aprovecha dos rutas de código distintas en la lógica de deserialización de Flight de React.

El primero es Chunk.prototype.then, que determina cómo se comportan los chunks como objetos «thenable». Cuando se aplica «await» a un chunk en estado INITIALIZED, se invoca a resolve(chunk.value). Si chunk.value es a su vez un objeto «thenable» (un objeto con un método .then() ), el operador «await» invoca de forma recursiva a chunk.value.then(). Esta resolución recursiva es el mecanismo mediante el cual un atacante redirige la ejecución a una función arbitraria.

El segundo es el controlador del prefijo $B (blob) dentro de la función parseModelString():

Figura 18: $B (blob) en la función parseModelString

En el caso $B, la función llama a response._formData.get(response._prefix + id). Tanto _formData.get como _prefix son propiedades del objeto _response almacenado dentro del fragmento. Al controlar estas propiedades mediante el recorrido de la cadena de prototipos, un atacante puede redirigir esta llamada para invocar el constructor global Function con código arbitrario como argumento.

Explotación

Through prototype chain traversal, an attacker reaches the global Function constructor via the path <any_object>.constructor.constructor. Because Chunk.prototype.then is a function, the path $1:constructor:constructor resolves to the global Function constructor, which accepts a string and returns a callable function containing that code.

Figura 19: Constructor de la función global

Para demostrar el impacto potencial en el mundo real, nuestros becarios han creado una carga útil de prueba de concepto que coincide con el análisis documentado de forma independiente por varios equipos de investigación en seguridad. El exploit funciona en dos fases:

Fase 1: Crear el fragmento ficticio:

The object delivered in field 0 acts as a fake chunk. Its then property is set to Chunk.prototype.then via the reference path $1:__proto__:then, allowing the Flight deserialization engine to invoke prototype-level behavior on this attacker-constructed object. The _response._formData.get property is pointed at the global Function constructor via $1:constructor:constructor, and _response._prefix is set to the malicious JavaScript code. The value field contains the string {"then": "$B0"}, instructing the blob handler to invoke itself on the same chunk when resolved. The status field is set to resolved_model so that initializeModelChunk is triggered when .then() is called, causing value to be parsed and the blob handler to fire.

Dado que el campo 1 aún no se ha recibido en este momento, el Server de React crea Server las funciones de devolución de llamada «resolve» y «reject» para gestionar la referencia pendiente.

Fase 2: Resolución del desencadenante:

Una vez que se entrega el campo 1 —que contiene «$@0», una referencia sin procesar al fragmento 0—, el fragmento pendiente se resuelve y apunta directamente al fragmento falso. Esto activa wakeChunk, que procesa las funciones de devolución de llamada en cola e inicia el recorrido de la cadena de prototipos durante la resolución de la referencia. Una vez que el fragmento falso se ha resuelto por completo, se vuelve a llamar a wakeChunk. Dado que la llamada de retorno de resolución del fragmento falso es la función de resolución implícita de Node.js, esta invoca el método .then() del fragmento y resuelve su valor, construyendo y ejecutando finalmente el código malicioso inyectado a través del constructor Function.

Para llevar a cabo el exploit completo solo se necesita una única solicitud HTTP:

Figura 20. Solicitud maliciosa

Replacing {{COMMAND}} with any JavaScript code executes it on the server. The reason: -1 field prevents a toString() error during processing. The Next-Action header may contain any arbitrary value - even x - because the vulnerable deserialization occurs before the server validates the requested Server Function. This is what makes the vulnerability pre-authentication: the payload is processed during the deserialization phase, before any application-level authentication or authorization logic is reached.

Si la vulnerabilidad se aprovecha con éxito, el atacante obtiene acceso completo al contexto de ejecución de Node.js en el servidor, lo que incluye acceso a `child_process` para ejecutar comandos de shell, a las variables de entorno que contienen credenciales de bases de datos y API , al sistema de archivos local y a los puntos de conexión de metadatos en la nube que permiten el movimiento lateral.

Prueba de concepto

Nuestros investigadores reprodujeron la vulnerabilidad en un entorno de laboratorio controlado utilizando una aplicación estándar de Next.js generada con create-next-app y preparada para producción, sin realizar modificaciones en la configuración predeterminada. La reproducción confirmó que la carga útil de explotación de una sola solicitud descrita anteriormente permite ejecutar código de forma remota de manera fiable.

Figura 21. Aplicación web Next.js vulnerable
Figura 22. El atacante compromete el servidor Next.js vulnerable

La demostración controlada puso de manifiesto que un atacante con acceso a la red de un servidor Next.js vulnerable puede ejecutar código Node.js arbitrario —incluida la creación de un shell inverso mediante `child_process.exec()`, la lectura de variables de entorno y el acceso al sistema de archivos local— sin necesidad de proporcionar credenciales ni activar comprobaciones de autenticación a nivel de la aplicación. El valor arbitrario aceptado para el encabezado Next-Action confirma aún más la naturaleza previa a la autenticación de la vulnerabilidad: el servidor procesa y deserializa la carga útil antes de realizar cualquier búsqueda de acción o comprobación de autorización.

Mitigación

El equipo de React publicó parches el 3 de diciembre de 2025, el mismo día en que se dio a conocer públicamente la vulnerabilidad. Las versiones corregidas están disponibles como React 19.0.1, 19.1.2 y 19.2.1. El parche añade una validación estricta de propiedades en getOutlinedModel() y reviveModel(), bloqueando explícitamente la resolución de propiedades de prototipo heredadas —incluidas __proto__, constructor y prototype— a partir de rutas de referencia controladas por el usuario en las cargas útiles de Flight.

Las organizaciones deben adoptar las siguientes medidas inmediatas:

  1. Actualiza los paquetes de React a una versión parcheada (19.0.1, 19.1.2 o 19.2.1) ejecutando «npm install react-server-dom-webpack@latest», «react-server-dom-parcel@latest» o «react-server-dom-turbopack@latest», según corresponda.
  2. Actualizar las dependencias de los marcos de trabajo: Next.js, React Router, Waku y otros marcos afectados han publicado los parches correspondientes. Consulta el aviso del equipo de React para conocer las rutas de actualización específicas para cada versión.
  3. No confíes únicamente en las medidas de mitigación de los proveedores de alojamiento: aunque proveedores como Vercel implementaron reglas WAF temporales tras la divulgación, se trata de medidas provisionales que no sustituyen a la aplicación de parches en los paquetes subyacentes.
  4. Revisa los registros del servidor en busca de solicitudes POST que incluyan encabezados «Next-Action» con cuerpos «multipart/form-data» que contengan los patrones $@ o __proto__, y supervisa si hay llamadas inesperadas a «child_process» o «execSync» en los registros de la aplicación.

Mitigación con OPSWAT

OPSWAT , una tecnología propia de la plataforma MetaDefender™, ofrece la visibilidad y el control necesarios para defenderse contra vulnerabilidades como la CVE-2025-55182. Dado que esta vulnerabilidad reside en paquetes npm de código abierto (react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack), las organizaciones deben, en primer lugar, elaborar un inventario completo de dónde se han implementado estos componentes en su infraestructura antes de que sea posible una corrección efectiva.

Figura 23. El SBOM detecta CVE-2025-55182

OPSWAT genera un inventario exhaustivo de todos los componentes de software, bibliotecas, contenedores y dependencias en uso. Al analizar aplicaciones o imágenes de contenedores que incluyen paquetes de React vulnerables, el sistema marca automáticamente CVE-2025-55182 como «Crítico» y ofrece orientación sobre las versiones corregidas disponibles, lo que permite a los equipos de seguridad priorizar y aplicar las medidas correctivas antes de que se produzca la explotación.

OPSWAT está disponible tanto en MetaDefender —para analizar aplicaciones individuales e imágenes de contenedores— como en MetaDefender Software Chain™ —para obtener visibilidad a nivel de proceso en todo el ciclo de vida del desarrollo—. En conjunto, permiten a los equipos de seguridad:

  • Localice rápidamente los componentes vulnerables: identifique de inmediato qué aplicaciones y contenedores incluyen los paquetes react-server-dom-* afectados en versiones vulnerables, asegurándose de que no se pase por alto ninguna implementación.
  • Garantizar la aplicación proactiva de parches: supervisar continuamente las dependencias de código abierto para detectar paquetes obsoletos o inseguros a medida que se publican nuevos avisos de seguridad, reduciendo así el tiempo de exposición.
  • Garantizar el cumplimiento normativo y la transparencia de la cadena de suministro: cumpla los requisitos normativos manteniendo un registro auditable de todos los componentes de software y sus vulnerabilidades conocidas.

La rapidez y la magnitud de los ataques dirigidos a la vulnerabilidad CVE-2025-55182 —más de 582 millones de intentos solo en la primera semana— ponen de manifiesto por qué las actualizaciones reactivas ya no son suficientes. Saber qué se tiene, dónde se ejecuta y cuándo se vuelve vulnerable es la base de una defensa proactiva. Esa visibilidad comienza con la SBOM.

Referencias

¡Mantente al día con OPSWAT!

Regístrate hoy mismo para recibir las últimas novedades de la empresa, historias, información sobre eventos y mucho más.