React RCE 事件留下的教訓:為何現在需要 HMAC 簽名、金鑰輪替與零信任
RCE 漏洞所揭示的:"一旦相信資料就結束了"
最近在 React Server Components/Next.js 中發生的 RCE 漏洞(CVE-2025-55182)不應該只被視為「React 被突破」的新聞。 這件事傳遞的核心訊息更具原則性。
「只要一次相信客戶端送來的資料,終究會被突破。」
這次漏洞的本質在於,伺服器在沒有充分驗證的情況下,直接使用客戶端送來的 Flight 協議資料(metadata) 進行模組載入與物件存取。
- 「這個值由 React 自動安全處理。」
- 「我們沒有直接使用的 API,應該沒問題。」
這種隱式信任累積到最後,導致 在未經預先認證的情況下就能執行 RCE 的點。
現在我們需要改變問題的角度。
- 「React 為什麼被突破?」
- 「我們現在在沒有任何簽名或驗證的情況下,信任哪些資料?」
在尋找答案的過程中,自然會出現 HMAC 簽名 與 零信任 這兩個概念。
為什麼 HMAC 簽名很重要:"這資料真的由我們的伺服器產生嗎?"
實際系統往往比想像中更依賴「伺服器之間的信任」。
- 前端 ↔ 後端
- 微服務 A ↔ B
- 後端 ↔ 後端(非同步工作、佇列、Webhook、內部 API 等)
在這些交互中,資料往往被假設為安全。
「這個 URL 沒有對外公開,只有內部系統會呼叫。」
「這個 token 格式只有我們的服務知道,安全。」
但攻擊者總是從打破這些「假設」開始。
此時,HMAC(如 HMAC-SHA256) 的存在是為了回答一個根本問題。
「這個請求/訊息,真的由「我們擁有秘密金鑰」的人產生嗎?」
更具體來說:
- 只有知道 HMAC 金鑰的一方才能產生 有效簽名(signature)。
- 伺服器接收
payload + signature後, - 可以確認「簽名是否正確」
- 確認「中間是否被篡改」
沒有 HMAC 簽名的世界
如果 REST/Webhook/內部 API 沒有 HMAC 簽名,會發生什麼?
- 攻擊者假設已經知道 URL 結構與參數。
- 他們可以隨意造請求,並讓伺服器誤以為是「自然的內部請求」。
例如:
POST /internal/run-action
Content-Type: application/json
{
"action": "promoteUser",
"userId": 123
}
即使原本是「只在後台呼叫的內部端點」,只要
- 網路邊界被突破,
- 內部代理存在漏洞,
- CI/CD、日誌、範例程式碼、文件被外部曝光,
攻擊者就能假裝內部系統呼叫此端點。
此時,如果有 HMAC 簽名,情況就完全不同。
- 對整個請求體加上 HMAC 簽名,
- 伺服器每次都驗證
signature,
「即使知道 URL、參數,沒有金鑰也無法產生有效請求」
範例:用 HMAC 簽名保護內部動作
以下示範一個簡單範例(TypeScript/Node.js 風格的偽程式碼)。
1. 伺服器擁有共享秘密金鑰
const HMAC_SECRET = process.env.HMAC_SECRET!; // 從 .env 或 Secret Manager 注入
2. 客戶端(或內部服務)產生請求時
import crypto from 'crypto';
function signPayload(payload: object): string {
const json = JSON.stringify(payload);
return crypto
.createHmac('sha256', HMAC_SECRET)
.update(json)
.digest('hex');
}
const payload = {
action: 'promoteUser',
userId: 123,
};
const signature = signPayload(payload);
// 傳送
fetch('/internal/run-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': signature,
},
body: JSON.stringify(payload),
});
3. 伺服器驗證時
function verifySignature(payload: any, signature: string): boolean {
const json = JSON.stringify(payload);
const expected = crypto
.createHmac('sha256', HMAC_SECRET)
.update(json)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex'),
);
}
app.post('/internal/run-action', (req, res) => {
const signature = req.headers['x-signature'];
if (!signature || !verifySignature(req.body, String(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 到這裡代表:
// 1) payload 未被篡改
// 2) 由知道 HMAC_SECRET 的主體產生
handleInternalAction(req.body);
});
這個結構即是「這資料真的由我們發送」的最基本證明模式。
若有多位管理員(運維/開發者):HMAC 金鑰輪替是「非選擇而是必須」
HMAC 金鑰的另一個實際問題是「人」。
- 運維、SRE、後端開發者、外包人員、實習生…
- 不是每個人都能存取
.env,但 - GitOps、CI/CD、文件、Slack、Notion 等任何地方都有可能洩露金鑰。
關鍵在於:
「金鑰可能洩露」這個假設必須成為設計的前提。
也就是說,「金鑰最終會洩露」,設計時必須預先考慮。
因此,HMAC 金鑰輪替(rotation) 成為必須。
為什麼需要輪替
-
人員變動 * 離職、團隊調動、外包結束等。 * 過去能看到金鑰的人,現在不應再能看到。
-
無法完全追蹤金鑰洩露 * 可能在 Slack DM、本地筆記、截圖、備忘錄中。 * 「從未洩露」的保證幾乎不可能。
-
事故後仍需可恢復 * 若某時點之前的金鑰洩露, * 必須能宣告「從此以後只接受新金鑰簽名的請求」。
實務中的 HMAC 金鑰輪替模式
對於多位管理員的組織,通常建議以下模式。
-
金鑰版本管理 * 例如
HMAC_SECRET_V1、HMAC_SECRET_V2, * 在請求標頭或負載中傳送kid(key id)。 -
伺服器同時持有多個金鑰,驗證時按版本處理 * 例如:先驗證 V1、V2, * 一段時間後棄用 V1,只允許 V2。
-
輪替流程文件化 * 「產生新金鑰 → 部署 → 兩邊(產生者/驗證者)都切換到新金鑰 → 刪除舊金鑰」 * 用 checklist + runbook 形式,確保人員變動時仍能跟隨。
-
權限分離 * 產生/管理金鑰的人與 * 編寫應用程式碼的人分離,若可能的話。
當 HMAC 金鑰成為「一旦設定就不變」的值時, 金鑰洩露即等於整個系統崩潰。
金鑰輪替就是把這個風險隨時間限制的技術手段。
零信任:"信任不是結構,而是每一次請求都要建立"
最後,React RCE 事件再次提醒我們的安全基礎原則。
安全的基礎是零信任(Zero Trust)。
零信任的核心非常簡單。
- 「因為是內部所以沒問題」
- 「前端只用的值所以安全」
- 「我們自己的框架所以自動保護」
這些說法必須根本禁止。
相反,我們應該問:
- 「這個輸入,是否在假設可能是惡意的情況下設計?」
- 「這個請求真的來自我們預期的主體嗎?」
- 「這個資料在傳輸過程中是否被篡改?」
- 「如果金鑰洩露,影響範圍是什麼,如何回收/恢復?」
React RCE 事件也可以從這個框架來看。
- RSC Flight 資料被「React 會給的」這種信任處理。
- 結果,客戶端可操縱的序列化資料直接影響伺服器模組載入路徑。
從零信任角度看,這種結構本應一開始就被視為風險訊號。
「客戶端可操縱的序列化資料影響伺服器執行路徑 = 必定風險」
而在這種情況下,最先想到的防禦措施就是:
- HMAC 簽名(完整性與來源驗證)
- 金鑰輪替(假設金鑰洩露時限制損失)
總結:"不要信任,讓它證明自己"
本文想表達的核心訊息只有三點。
-
基本上不要信任資料 * 即使是「框架內部使用的資料」,只要經過網路, * 也可能成為攻擊面。
-
HMAC 簽名是「這資料真的來自我們」的基本工具 * REST API、Webhook、內部 RPC、非同步訊息等 * 所有伺服器間通訊都應考慮 HMAC。 * 若管理人員多,金鑰輪替必須成為流程。
-
安全的基礎是零信任 * 「內部就好」的說法即為風險訊號。 * 信任必須在每一次請求、每一筆資料中透過驗證建立。
React RCE 事件提醒我們,在前後端邊界模糊的時代, 我們需要採取的態度。
「不要信任,驗證。將驗證寫進程式、流程、金鑰管理政策。」
第一步就是 HMAC 簽名與金鑰輪替, 其上構築的哲學即是 零信任。

目前沒有評論。