# AfterPack 威胁模型：受防御的攻击面

AfterPack 的安全评审材料：它防护的资产、每类攻击者能还原什么、秘密如何保住、当前范围止于何处，以及与它并用的控制措施。

Source: https://www.afterpack.dev/zh/docs/threat-model

AfterPack 防护一项资产：**你的构建产出的 JavaScript 产物**。它重写这个产物，让阅读、提取指纹或打补丁都要攻击者付出真正的工作量，并让针对某次构建做的工具到下一次构建就失效。

本页供安全评审阅读。它说明受防护的范围、每类攻击者能还原什么、构建期控制在哪一点失效，以及覆盖其余部分的控制措施。

## 受防御的攻击面

| 防御对象 | 它排除掉的攻击 | 起作用的属性 |
|---|---|---|
| 你发布的 bundle 的可读性 | 把 bundle 粘贴进某个工具或模型，拿回你的源代码 | 标识符被重命名，字符串被编码，控制流被扁平化，恒等式被融合，因此把 bundle 读回你的源代码需要真正的、每次构建都要重来的功夫 |
| 你的 bundle 的*结构* | 只写一次去混淆器、特征签名或补丁脚本，然后在各次发布间反复使用 | 每次构建一个新的 [`seed`](https://www.afterpack.dev/zh/docs/config#seed)，所以针对某次构建的模式编写的工具到下一次就会失效 |
| 你的实现的归属 | 证明某个已发布的 bundle 是你的代码、你的库或你的算法 | 静态阅读者必须在每次构建上重做重命名与编码的还原工作，才能把结果与你的代码对应起来；能够彻底消除这种对应关系的、破坏来源信息的类别目前不会产生 |
| 你用于比较的字符串 | 在 bundle 中 grep 一个令牌或门控字符串 | [字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)，以及针对相等性门控的[非物化](#protecting-a-secret)。在 `light` 下，数字仍然可读 |
| 使用中产物的完整性 | 修补一处校验，然后发布打过补丁的构建 | 新的 [`seed`](https://www.afterpack.dev/zh/docs/config#seed) 让补丁位置每次发布都改变。[自完整性检查](#anti-tamper)只检测对它自身校验表的修改 |

下文的所有内容都是对这五行的限定：需要击败的是谁、每个属性能覆盖多远，以及它覆盖不到什么。

## 攻击者类别

是两条轴，而不是一把尺子。**[静态攻击者](https://www.afterpack.dev/zh/docs/reversal-classes#static-attacker-defined)**阅读字节：grep、AST 工具、一遍去混淆器处理，或者一个不执行代码、只解释代码的前沿模型。**动态攻击者**运行产物：在打过补丁的运行时下、勾住内置对象，或者针对离线回放的已录制执行轨迹。一种变换可以对阅读者很强，而对运行者很弱，把两者混为一谈是这一领域最常见的过度宣称。

下面的阶梯按攻击者能力排序。代价是相对于其他行而言的，不是一个绝对数字。

| 级别 | 攻击者 | 范围 | 攻击者的代价 | 结果 |
|---|---|---|---|---|
| **A0** | 静态模式工具：grep、AST 匹配器、确定性的去混淆处理 | 单次构建 | 微不足道 | 能还原仍为明文的内容。现成的去混淆器在 grep 之外没有额外收获（[测量结果](#measured-results)）。 |
| **A1** | 盲读模型：只读一次，不执行，不用工具 | 单次构建 | 微不足道 | 读到的大部分内容都被还原了：在三个小程序上，它在 `light` 下完整还原了全部三个，在 `medium` 下两个，在 `extreme` 下一个；没能重建出可运行程序的地方，它仍还原了大部分埋入的秘密（[测量结果](#measured-results)）。 |
| **A2** | 能执行代码、使用工具并有预算的智能体模型 | 单次构建 | 低，按构建计 | 对该次构建失守：运行代码就能看到它使用的值。基于该次构建形态做的工具，下一次要重做。 |
| **A3** | 在大量 AfterPack 自身输出样本上训练过的模型 | 整个输出分布，而不是单次构建 | 一次性投入，之后每次构建几乎为零 | 未测量。见下文。 |
| **A4** | 插桩的动态分析：勾住内置对象、录制执行、堆快照 | 单次构建，可以低成本重复 | 分析师的时间，按构建计 | 字符串层面失守：一个运行时钩子在每个预设下都还原出了 97 个中的 96 个（[实测结果](#measured-results)）。见[边界](#the-boundary)。 |
| **A5** | 特权运行时：打过补丁的浏览器、JavaScript 层之下的原生钩子 | 单次构建 | 前期搭建成本高，之后几乎为零 | 失守，对任何客户端内容都是如此。 |

各级别在能力上是累积的，在可能性上则不是。A0 覆盖了针对已发布 JavaScript 的绝大多数真实攻击：有人运行一个免费工具，或者把你的 bundle 粘贴进聊天窗口。A5 是罕见而昂贵的终局，大多数产品永远不会遇到。

**A0 到 A2 攻击的是单次构建。**除非你固定种子，每次构建都会用新的 [`seed`](https://www.afterpack.dev/zh/docs/config#seed) 改变形态，所以针对某次构建写的识别器或补丁，到下一次都得重写。它并不隐藏值：代码使用的任何值，运行代码的人都能读到。

**A3 攻击的是整个输出分布。**在足够大的样本上训练过的模型，会学会对逐构建差异保持不变，而轮换种子对它毫无影响。仍然有用的是瞄准：[Pro](https://www.afterpack.dev/zh/docs/pro#what-pro-buys) 对你[标记的区域](https://www.afterpack.dev/zh/docs/directives#what-you-can-set)使用更重的预设，并增加两种需手动开启的变换类型：[`transforms.selfIntegrity.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled) 和 [`transforms.comparisonHardening.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled)。标记真正重要的部分；参见[最佳实践](https://www.afterpack.dev/zh/docs/best-practices)。

**A4 和 A5 完全绕开了静态产物。**[边界](#the-boundary)一节涵盖了这两者。

### 测量结果

每类攻击者从引擎 0.2.2 的输出中还原出了什么。测量于 2026 年 10 月，对象是五个埋入了秘密的程序和真实的生产 bundle。前沿模型这一行来自 2026 年 10 月在引擎 0.2.6 上单独进行的一次测量。

| 攻击者 | `light` | `medium` | `hard` 和 `extreme` |
|---|---|---|---|
| 模式工具：webcrack、restringer、synchrony、terser | 不比 grep 多 | 不比 grep 多 | 不比 grep 多 |
| 用 grep 查找 42 个埋入的数字 | 42 | 18 | 18 |
| 对输出做静态求值 | 92–100% 的字符串 | 先做一遍保持行为不变的清理，再还原 96–98% 的字符串 | 同样清理后还原 96–98% 的字符串 |
| 挂在内置函数上的 15 行运行时钩子 | 97 个字符串中的 96 个 | 97 个字符串中的 96 个 | 97 个字符串中的 96 个 |
| 前沿模型，盲读，无工具，不执行 | 3 个程序全部完整还原 | 3 个中 2 个完整还原；第三个泄露了 17 个秘密中的 12 个，但没有重建出可运行的程序 | 仅 `extreme`：3 个中 1 个完整还原（9 个秘密全部还原）；另外两个分别泄露了 17 个中的 12 个和 3 个中的 2 个秘密，都没有重建出可运行的程序 |

在前沿模型这一行中，Claude Opus 5.5 只拿到混淆后的文件，它拒绝处理的 5 个文件改由 Claude Opus 4.8 处理；“完整还原”指重建的代码打印出与原程序相同的测试向量，并包含每一个埋入的秘密。注意事项：语料很小，只有三个程序；每个程序和预设只尝试一次；攻击者只有 Claude。

## 混淆保护的是什么

AfterPack 追求两项性质。第一是**代价**：还原一次构建的代价应高于它所保护的价值。第二是**发布节奏优势**：你的更新速度快于攻击者的工具泛化到下一次构建的速度。没有任何混淆器（包括 AfterPack）能让还原变得不可能，本页的任何内容都不应被理解为这样的承诺。

[默认预设](https://www.afterpack.dev/zh/docs/presets#the-default-is-light)会重命名标识符并编码字符串字面量。从 `medium` 起，AfterPack 做的不止于此：控制流被扁平化，恒等式被融合，逻辑被重写为现成工具无法还原回去的等价形式。还原这份输出的攻击者可以恢复出一个可以运行的程序，但前提是完成这些变换所要求的真正的模式识别工作，而针对某一次构建的形态编写的工具，到下一次构建就会失效。[还原类](https://www.afterpack.dev/zh/docs/reversal-classes)把这一点拆分成静态阅读者所面对的几个类别。

逐构建的 [`seed`](https://www.afterpack.dev/zh/docs/config#seed) 默认随机，所以同一份源码的两次构建在每一处结构上都不同。针对第 N 次构建的模式编写的工具会在第 N+1 次失效，而有人已经从第 N 次构建中读到的值依然是读到了。只有轮换构建，这一性质才有价值；[最佳实践](https://www.afterpack.dev/zh/docs/best-practices)和[构建与 CI](https://www.afterpack.dev/zh/docs/builds) 介绍了如何把重新构建接入发布流水线。

这些本身都藏不住密钥。那需要另一种机制。

## 保护密钥

任何行为与你的程序相同的程序都会吐出密钥，包括静态阅读者从一个受保护区域中恢复出的程序。重塑密钥周围的代码对它毫无作用。

让密钥免于被阅读字节的人获取的是**非物化**，在[密钥如何得以保留](https://www.afterpack.dev/zh/docs/concepts#how-secrets-survive)中有介绍。提供这一能力的设置 [`transforms.comparisonHardening.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled) 是一项 Pro 设置，默认关闭，在每个预设级别下的效果都相同。

面对运行你代码的人，适用的是[边界](#the-boundary)一节。逐构建的差异会在每次轮换时提高重新提取密钥的代价。而针对 JavaScript 引擎的通用运行时钩子，其代价不受影响，因为它并不依赖 AfterPack 的结构。[AfterPack 的工作原理](https://www.afterpack.dev/zh/docs/concepts#how-secrets-survive)把同一机制与标准输出已经做到的事放在一起说明。

## 边界

在被插桩的运行时下运行你代码的攻击者，能拿到代码本身用到的任何值。只要运行中的程序需要它，一个坚定的动态攻击者就能观察到它。AfterPack 让这个值难以定位，并让你能以很低的代价把它[轮换](https://www.afterpack.dev/zh/docs/config#seed)掉。它不会让这个值变得无法恢复。

> **不要把服务端的权威放到客户端**
>
> 签名密钥、长期有效的凭据，以及任何攻击者可以重复使用或转卖的东西，都应该放在服务端，无论你使用哪种混淆器。

这一限制并非 AfterPack 独有。一旦攻击者控制了运行时，[每一个构建期混淆器](https://www.afterpack.dev/zh/docs/comparison)都会撞上同一堵墙。越过这堵墙后，AfterPack 仍然保留的是下一次发布的形态：针对某次构建做的工具，到下一次还得重做。

## 防篡改

自完整性检查 [`transforms.selfIntegrity.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled) 会对它自身的校验表加一道检查。它只检测对这张表的修改。对你代码中其他任何地方的修改，例如改成 `return true` 的补丁，都不会被检测到。

它也检测不到被动观察：记录值的钩子、堆读取或原生插桩都不会改变它检查的内容。这是一项 Pro 设置，默认关闭且全局生效，需要附着在 `medium` 或更高级别的代码上，因此 `light` 构建只在被提高到这一级别的区域内带有它。[`preset=hard`](https://www.afterpack.dev/zh/docs/config#preset) 不会开启它，而是把一个区域提高到[复杂度目标](https://www.afterpack.dev/zh/docs/config#complexity) 12，并开启[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)。

## AfterPack 不能替代什么

AfterPack 是整套控制措施中的一项，安全评审应该看清它的边缘在哪里。下面每一行说明了构建期控制措施当前的范围，以及覆盖其余部分的控制措施。范围会随产品的发布而变化；[公开路线图](https://www.afterpack.dev/zh/roadmap)是 AfterPack 说明未来计划的唯一场所。

| 控制措施 | AfterPack 目前的贡献 | 覆盖其余部分的是什么 |
| --- | --- | --- |
| **请求层防御**（机器人、爬取、滥用） | 想从你的 bundle 中取出结构化数据的爬虫，必须[先还原它](https://www.afterpack.dev/zh/docs/reversal-classes)。AfterPack 作用于产物，看不到请求。 | 挑战-应答服务（reCAPTCHA、hCaptcha、Arkose），加上按 IP、会话或 API 密钥进行的服务端速率限制。 |
| **客户端完整性**（作弊、自动化、打过补丁的客户端） | 面对一个 [`preset=hard`](https://www.afterpack.dev/zh/docs/config#preset)、[逐构建多态](https://www.afterpack.dev/zh/docs/config#seed)的客户端，作弊或自动化脚本的作者每次发布都要重新做一遍真正的分析，而不是一次性地修补一个已知偏移量。AfterPack 提高这一代价；它不做裁决。 | 由服务端权威校验命中、状态转换和结果，客户端只用于呈现服务端已经做出的决定。 |
| **交付完整性**（代理、CDN、扩展程序） | AfterPack 保护它产出的字节。它控制不了这些字节的传输路径，而使用浏览器自身请求拦截 API 的扩展程序，会在任何 JavaScript 运行之前就决定加载什么。 | 子资源完整性（`integrity="sha384-…"`），让浏览器拒绝与你构建时的哈希不匹配的 bundle，再加上对任何中间方可能影响的内容做服务端校验。 |
| **应用安全**（bug、注入、失效的认证） | 混淆会拖慢通过阅读源码发现漏洞的速度。它不会消除漏洞，而任何通过运行中的程序触达漏洞的人根本不会去读你的源码。 | 修复 bug。[`npx afterpack@latest audit`](https://www.afterpack.dev/zh/docs/audit) 会扫描一个线上站点已经在泄露的内容。 |
| **关于客户端的信任决策** | [逐构建多态性](https://www.afterpack.dev/zh/docs/config#seed)把一次性的补丁变成一笔反复发生的重新推导代价。 | 下文的[服务端权威校验](#not-a-substitute-for-server-authoritative-validation)。 |

### 不能替代服务端权威校验

上面每一行都有一个共同的根源：一个必须真正生效的许可证校验、支付门控、权益规则或功能开关，都是客户端对自己做出的决定，而控制这个客户端的人就能伪造它。这是客户端执行的属性，而不是某个特定混淆器的缺陷，任何构建期变换都无法弥补它。

对于任何必须在敌对客户端面前站得住的东西，比如许可、支付、权益或对局结果，请在服务端做出决定，只让客户端展示它。AfterPack 的贡献是[经济上的](#what-obfuscation-protects)，而且是实实在在的：它提高了攻击确实运行在客户端上的那部分内容的反复代价。

## 下一步

- [攻击者能恢复什么](https://www.afterpack.dev/zh/docs/reversal-classes)：深入讲解静态攻击者类别，附示意图。
- [AfterPack 的工作原理](https://www.afterpack.dev/zh/docs/concepts)：标准输出已经对你的 bundle 做了什么。
- [Protection Map](https://www.afterpack.dev/zh/docs/protection-map)：在你自己的构建中验证哪一类作用于哪一行。
- [最佳实践](https://www.afterpack.dev/zh/docs/best-practices)：标记什么、轮换什么、把什么留在服务端。
- [隐私与数据处理](https://www.afterpack.dev/zh/docs/privacy)：什么会离开你的机器、存储了什么，以及存储多久。
- [审计](https://www.afterpack.dev/zh/docs/audit)：扫描一个线上 URL，找出混淆无法挽回的泄露。
- [对比](https://www.afterpack.dev/zh/docs/comparison)：AfterPack 与你已经熟悉的工具有何不同。
- [层级](https://www.afterpack.dev/zh/docs/tiers)：Free 已经覆盖了哪些级别，以及 Pro 增加了什么。
