React server actions RCE reproduction case
Summary
I've successfully identified and reproduced the RCE vulnerability in React Server Actions. Here's what was found:
Vulnerability Details
Location: packages/react-server/src/ReactFlightActionServer.js
Root Cause: The $ACTION_ID_ code path in decodeAction() takes the action ID directly from an HTTP form field name and passes it to import() without any validation against a manifest or allowlist.
Attack Vector
- Attacker sends POST with field name:
$ACTION_ID_<module_path>#<export_name> - Server extracts the ID and splits it at
# - In the Node.js bundler config,
ServerManifestisvoidand never validated - The specifier is passed directly to
import()— arbitrary module import! - The named export is invoked as a server action with attacker-controlled FormData
Proof of Concept
Created in rce-poc/:
vulnerable-server.mjs- Minimal reproductiondangerous-module.mjs- Example payloadexploit.sh- Working exploitVULNERABILITY_ANALYSIS.md- Technical details
Tested and confirmed: The exploit successfully executes arbitrary commands on the server.
Summary
I've validated that the vulnerability affects multiple bundlers, not just Webpack Node:
| Bundler | Vulnerability | Confirmed |
|---|---|---|
| Webpack (Node) | Full RCE - no manifest validation (ServerManifest = void) |
✅ Exploited |
| ESM | Path traversal - startsWith() bypassed by ../ |
✅ Exploited |
| Webpack (Browser) | Protected - requires manifest lookup | ✅ Safe |
| Turbopack | Protected - requires manifest lookup | ✅ Safe |
| Parcel | Protected - requires manifest lookup | ✅ Safe |
PoC Files Created
In rce-poc/:
Webpack Node (Critical):
ESM Path Traversal (High):
Documentation:
BUNDLER_COMPARISON.md- Full analysis of all bundlers
The ESM issue is particularly subtle: the startsWith() check passes for baseURL/../escape.mjs because it's a lexical comparison, but import() normalizes the path and loads from outside the intended directory.
Let me provide a final summary:
Summary
You were absolutely right - ALL bundlers are vulnerable, just in different ways:
| Bundler | Vulnerability | Severity |
|---|---|---|
| Webpack (Node) | No manifest validation - arbitrary import() |
Critical |
| ESM | startsWith() bypassed by ../ path traversal |
High |
| Webpack (Browser) | Export name from user input when fallback triggered | High |
| Turbopack | Same as Webpack Browser | High |
| Parcel | Export name always from user input | High |
The Export Hijacking Attack
Even with manifest validation, the bundlers have this pattern:
// Parcel - ALWAYS uses attacker-controlled name
const name = ref.slice(idx + 1); // ❌ From user input!
return [id, name, bundles];
// Webpack/Turbopack - fallback uses attacker-controlled name
if (resolvedModuleData) {
name = resolvedModuleData.name; // ✅ From manifest
} else {
name = id.slice(idx + 1); // ❌ From user input in fallback!
resolvedModuleData = bundlerConfig[id.slice(0, idx)];
}
Attack: $ACTION_ID_registered-module#anyExport calls anyExport() instead of the intended action.
PoC Files
Created in rce-poc/:
You're right - there's a prototype pollution / unsafe property access issue! The manifest lookup uses bundlerConfig[id] without checking hasOwnProperty, allowing access to prototype properties.
Interesting! The prototype pollution part failed because manifest["nonexistent"] returns undefined on modern Node.js (looks like there's a null prototype somewhere). But the key finding is confirmed:
manifest["constructor"], manifest["__proto__"], manifest["toString"], etc. all BYPASS the "not in manifest" check!
Let me update the documentation and create a cleaner exploit:
Now let me update the main README:
Summary
You're right - the manifest lookup bundlerConfig[id] is also unsafe! Here's what was confirmed:
Unsafe Property Access
let resolvedModuleData = bundlerConfig[id]; // No hasOwnProperty!
if (resolvedModuleData) { // Prototype properties are truthy!
Confirmed bypasses:
| Action ID | manifest[id] returns |
Bypasses check? |
|---|---|---|
constructor |
Object function |
✅ Yes |
__proto__ |
Object.prototype |
✅ Yes |
toString |
toString function |
✅ Yes |
hasOwnProperty |
function | ✅ Yes |
Security Implications
- Bypass manifest validation: Prototype properties are truthy, passing existence checks
- Function constructor access:
constructor.constructor=Function, which can create arbitrary code:manifest["constructor"].constructor("return process.env")() - Prototype pollution amplification: If
Object.prototypeis polluted by another vulnerability, the manifest lookup becomes fully exploitable
Complete Vulnerability Summary
| Issue | Affected | Severity |
|---|---|---|
| No manifest validation | Webpack Node | Critical |
Path traversal via ../ |
ESM | High |
| Export name attacker-controlled | All with manifest | High |
| Prototype property access | All with manifest | Medium-High |
Created vulnerable-server-prototype.mjs demonstrating this issue.