React RCE 事件留下的教训:为什么现在需要 HMAC 签名、密钥轮换和零信任
RCE 漏洞展示的真相:"一旦信任数据,终将被突破"
最近在 React Server Components/Next.js 中出现的 RCE 漏洞(CVE-2025-55182)并不只是“React 被突破”的新闻。它所传递的核心信息更具原则性。
“只要一次信任客户端发送的数据,终将被突破。”
此次漏洞的根本原因是服务器在没有充分验证的情况下,直接使用了客户端发送的 Flight 协议数据(元数据) 进行模块加载和对象访问。
- “这个值由 React 自动安全处理。”
- “我们没有直接使用的 API,应该没问题。”
这种隐式信任的累积,最终导致了 在未进行预认证的情况下就能实现 RCE 的点。
现在我们需要改变提问方式。
- “React 为什么被突破?”
- “我们现在在没有任何签名或验证的情况下,信任哪些数据?”
在寻找答案的过程中,自然而然会出现 HMAC 签名 和 零信任 这两个概念。
为什么 HMAC 签名很重要:"这数据真的来自我们的服务器吗?"
现实中的系统往往过度依赖“服务器之间的信任”。
- 前端 ↔ 后端
- 微服务 A ↔ B
- 后端 ↔ 后端(异步任务、队列、Webhook、内部 API 等)
在这些交互中,数据往往被假设为安全的。
“这个 URL 没有暴露给外部,只有内部系统会调用。”
“这个 token 格式只有我们服务知道,安全。”
但攻击者总是从打破这些“假设”开始。
此时,HMAC(如 HMAC-SHA256) 的存在是为了回答一个根本性的问题。
这请求/消息,是否真的由“我们拥有的秘密密钥”的人创建?
更详细地说:
- 只有知道 HMAC 密钥的一方才能生成 有效签名(signature)。
- 服务器接收
payload + signature,即可验证 - “签名是否正确”,
- “中间是否被篡改”。
没有 HMAC 签名的世界
没有 HMAC 签名的 REST/Webhook/内部 API 会怎样?
- 假设攻击者已经了解了 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 签名与密钥轮换,构建在其上的哲学即是 零信任。

目前没有评论。