
Este repositorio contiene una reproducción PoC de CVE-2025-55182, una vulnerabilidad de seguridad crítica en React Server Components (RSC) que permite la ejecución arbitraria de código sin autenticación.
La vulnerabilidad existe en la forma en que React Server Components deserializa las "Server Actions" de las peticiones del cliente. En concreto, la función requireModule no validaba que el nombre de exportación solicitado fuera una propiedad directa del módulo. Esto permitía a los atacantes acceder a la propiedad constructor de las funciones exportadas, obteniendo una referencia al constructor global Function, que puede utilizarse para ejecutar código arbitrario.
Este PoC utiliza un entorno Node.js mínimo para aislar la vulnerabilidad en la librería react-server-dom-webpack, solo para asegurarse de que el exploit demuestra el fallo en la propia librería, NO una mala configuración en un framework.
npmnpm install
[!NOTE] El
package.jsonestá fijado a la versión vulnerable19.0.0.
Este script configura un servidor HTTP básico que utiliza el runtime de React vulnerable para decodificar las peticiones.
# tty1
node --conditions react-server server.js
En una terminal separada, ejecuta el exploit. Esto envía un payload Flight malicioso al servidor.
# tty2
node exploit.js id
Deberías ver la salida del comando devuelta en la respuesta:
Salida esperada:
Response: uid=0(root) gid=0(root) groups=0(root)
¿Por qué ocurrió la vulnerabilidad?
La función requireModule en ReactFlightDOMServerNode.js básicamente confiaba en cualquier name que enviara el cliente. Hacía moduleExports[metadata[NAME]] sin comprobar si esa propiedad realmente debía estar expuesta. Así que si el cliente decía «oye, quiero esta propiedad», el servidor respondía «¡claro! ahí la tienes, amigo».
¿Por qué es mala idea permitir que la gente acceda a cualquier propiedad?
Porque básicamente permite que cualquiera acceda a la cadena de prototipos, incluso al constructor, lo cual es super peligroso. Si el módulo exporta una función (como module.exports = () => {}), entonces su constructor es literalmente el constructor global Function.
¿Por qué obtener el constructor Function implica RCE?
Una vez que un atacante obtiene el constructor Function, puede abusar de la funcionalidad "Bound Server Action". Le vinculan una cadena que contiene JavaScript malicioso (básicamente convirtiéndolo en new Function("evil code")). Y una vez que se ejecuta, el servidor ejecuta cualquier código que hayan introducido.
¿Por qué React ejecutaría realmente esa función maliciosa?
Porque las Server Actions pueden activarse mediante un ID. Si el atacante crea un payload con un Action ID que apunte a su referencia module#constructor, React lo resuelve como una acción normal y lo ejecuta. Esa «acción» es en realidad su función maliciosa.
¿Por qué no se validó nada de esto?
El sistema simplemente asumía que id y name de los metadatos de Server Reference siempre harían referencia a exportaciones válidas definidas por el desarrollador. No había ninguna comprobación de seguridad como hasOwnProperty para asegurarse de que la propiedad solicitada fuera una exportación real y no algo heredado de la cadena de prototipos.
server.js en lugar de Next.js?Utilizo un server.js básico (y un helper webpack-runtime.js) para configurar manualmente el runtime de React Server Components. Esto nos permite:
module.exports = fn). Un bundler real podría cambiar cómo se envuelven las exportaciones, dependiendo de su configuración.react-server-dom-webpack, no en Next.js.react-server-dom-webpack asume que se ejecuta dentro de un bundle de Webpack. Nuestro webpack-runtime.js le proporciona los globales que espera (__webpack_require__, __webpack_chunk_load__).Esto no es simular la vulnerabilidad, es simplemente darle a la librería el runtime mínimo necesario para que realmente funcione.
Ha habido discusión sobre "PoCs inválidos" que solo funcionan si el desarrollador expone deliberadamente cosas peligrosas como child_process.exec.
Este PoC no es uno de esos. Funciona en una configuración normal y segura.
La función expuesta es inofensiva
La aplicación expone una función simple updateProfile que solo devuelve una cadena y nada sospechoso, sin comandos de shell.
El exploit escapa completamente de esa función
La vulnerabilidad permite al atacante ignorar la exportación segura e ir directamente a updateProfile.constructor, que es el constructor global Function.
El problema central es el acceso a la propiedad
React NO debería haber permitido el acceso a .constructor. El desarrollador no tenía la intención de exponer el constructor Function; en cambio, la deserialización insegura lo hizo por él.
El único requisito real es que el módulo exporte una función directamente (module.exports = fn), algo muy común en CommonJS y en muchas configuraciones de bundlers.
El payload en exploit.js construye un mensaje React Flight con tres chunks:
id: "user-profile-action#constructor", es decir, «dame el constructor».bound: apunta al Chunk 2, que contiene los argumentos.["console.log('nice try, diddy!')"]: la cadena de código malicioso.Cuando React deserializa esto:
user-profile-action..constructor => obtiene el Function global.new Function("console.log('nice try, diddy!')")¡Y eso es el RCE!
Actualiza inmediatamente a las versiones parcheadas:
react-server-dom-webpack >= 19.0.1react-server-dom-parcel >= 19.0.1react-server-dom-turbopack >= 19.0.1El parche introduce comprobaciones hasOwnProperty para evitar el acceso a propiedades heredadas y restringe la subida de archivos base64.
Si ejecutas este PoC contra una versión parcheada, el servidor fallará o dará un error como:
$ node --conditions react-server server.js
Listening on http://localhost:3000
/path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726
resolvedValue = resolvedValue.bind.apply(
^
TypeError: Cannot read properties of undefined (reading 'bind')
at /path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726:43
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v20.19.3
Esto confirma que el exploit no pudo acceder a la propiedad constructor (devolvió undefined en lugar de Function), por lo que la posterior llamada a .bind falló.
Este código es solo para fines educativos y de prueba. No utilices este exploit contra sistemas que no poseas o para los que no tengas permiso explícito de prueba.
Publicado bajo la DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE.