warden-for-js
v3.0.2
Published
Warden 是一个面向 Webpack 5 的浏览器端安全加固插件。目前主要用于浏览器扩展构建,通过三层互补机制收敛第三方模块可获得的能力:
Readme
Warden for JS
Warden 是一个面向 Webpack 5 的浏览器端安全加固插件。目前主要用于浏览器扩展构建,通过三层互补机制收敛第三方模块可获得的能力:
| 机制 | 保护目标 | 默认状态 |
| --- | --- | --- |
| 全局变量 Proxy 沙箱 | 限制第三方模块通过 globalThis、window、self 和 __webpack_require__.g 访问浏览器全局能力 | 开启 |
| Webpack require 保护 | 限制模块工厂只能加载依赖图或人工策略明确授权的模块 | 开启 |
| 关键原型冻结 | 防止敏感运行时依赖的关键内建对象和方法在业务启动后被替换 | 开启 |
Warden 属于现有 Webpack 运行时上的能力加固,不是独立 Realm,也不提供完整的 SES 式 JavaScript 隔离。
快速接入
const { WardenPlugin } = require("warden-for-js");
module.exports = {
// ...
plugins: [
new WardenPlugin({
dev: false,
}),
],
};可用选项:
export type WardenPluginOptions = {
dev?: boolean;
policyOverPath?: string;
policyOutputPath?: string;
freezePrototypes?: boolean;
prototypeFreezeMode?: "compat" | "strict";
protectWebpackRequire?: boolean;
forceCheck?: boolean;
};| 选项 | 默认值 | 说明 |
| --- | --- | --- |
| dev | true | 全局沙箱的开发模式。未声明的全局访问会被放行并记录到 globalThis.__dbg_ru1n;Webpack require 保护仍然严格阻断未授权导入 |
| policyOverPath | SecSDK/policy-over.json | 人工策略文件,相对于 Webpack context |
| policyOutputPath | SecSDK/policy.json | 构建生成的最终可读策略文件,相对于 Webpack context |
| freezePrototypes | true | 是否生成并加载关键原型冻结 bootstrap |
| prototypeFreezeMode | "compat" | 原型保护模式;compat 锁定既有成员并保留必要扩展点,strict 额外冻结 Object.prototype 并移除 Function.prototype.toString 兼容例外 |
| protectWebpackRequire | true | 是否保护模块工厂收到的 __webpack_require__ |
| forceCheck | !dev | 构建检查发现敏感全局权限或三方模块越界依赖时是否终止构建;生产模式默认终止,显式关闭时逐条 console.warn |
freezePrototypes 与 protectWebpackRequire 相互独立。关闭原型冻结不会关闭 require 保护,反之亦然。
一、全局变量 Proxy 沙箱
Warden 会分析并改写 node_modules 中实际参与打包的 JavaScript 模块。启用沙箱的模块会以类似下面的结构运行:
const g = __webpack_require__.g;
((globalThis, window, self) => {
// 原模块工厂代码
})(g, g, g);构建无法解析任何可用源码表示时会直接失败,不再静默生成空策略;多个可解析源码表示的访问结果取并集,避免 loader 前后表示差异漏掉能力。为控制运行时开销,静态可确定的裸全局标识符保持原生词法访问,不会统一改写成 globalThis[...];Proxy 保护仍针对全局对象别名及动态属性路径。
因此模块内通过 globalThis、window、self 或 Webpack 的全局对象 helper 发起的操作都会经过同一个 Proxy。globalThis.window、globalThis.self 和 globalThis.globalThis 也会继续返回当前模块的沙箱,而不是泄露真实全局对象。
沙箱工厂由 Warden 的 Webpack module-execution interceptor 私有持有。interceptor 根据当前 runtime 和 module ID 选择策略、创建沙箱,再通过模块私有且只读的 __webpack_require__.g 交给模块;页面全局和 Worker 全局均不会安装 __mkGb,模块也不能传入任意规则或其他 policy ID 自行创建沙箱。
权限模型
每个模块拥有独立的全局变量规则表:
r:允许读取、存在性检查和属性描述符查询。rw:包含读取权限,并允许赋值、删除和defineProperty。- 未声明属性:生产模式读取返回
undefined,写入被拒绝;开发模式放行并记录实际访问。 - 标准 ECMAScript 内建对象的读取默认放行,
eval除外。 - 已知浏览器全局函数会绑定到真实全局对象,以保持
setTimeout、fetch、addEventListener等宿主 API 的调用兼容性。
沙箱实现了以下 Proxy 陷阱:
get / set / has / deleteProperty / defineProperty
getOwnPropertyDescriptor / ownKeys
getPrototypeOf / setPrototypeOf
isExtensible / preventExtensions反射操作与普通属性访问使用同一套权限。Proxy handler 会冻结;传入的规则首先复制到 null-prototype 对象后再冻结,因此污染 Object.prototype 不能伪造模块权限。沙箱以真实 globalThis 为 target,但不会冻结这个 Proxy 或真实全局对象。
开发模式与生产模式
dev: true 用于发现缺失的全局权限:未声明访问会继续执行,并按模块记录到 globalThis.__dbg_ru1n。dev: false 才会执行严格的全局读写限制。
这个开关不影响 Webpack require 保护。无论开发或生产模式,未授权模块导入都会立即抛错。
二、Webpack __webpack_require__ 能力保护
开启 protectWebpackRequire 后,Warden 会为每个 Webpack runtime 安装模块执行 interceptor。模块工厂不会收到原始 require,而会收到只属于当前模块的受保护 Proxy。
构建阶段会根据最终 ModuleGraph、ChunkGraph 和模块 ID 生成授权关系:
- 同步依赖、动态 import、context、weak context、循环依赖等均从 Webpack 的实际连接中解析。
- inactive 连接不会进入授权表;
TRANSITIVE_ONLY连接会递归展开。 - 受全局 Proxy 保护的第三方模块会退出 scope-hoisting,保证一个模块工厂只绑定一份全局策略;其余 concatenated module 的最终工厂仍是 require 授权单元,权限为全部成员依赖的并集。
- 动态 chunk 后续注册的模块仍经过相同 interceptor。
- 应用模块、npm 模块、ContextModule、ExternalModule 等实际发射的 JavaScript 工厂都会被保护。
受保护 require 只允许加载授权模块,并只暴露当前模块代码生成阶段真正需要的 runtime helper:
.g是模块私有的虚拟属性,不再改写共享 Webpack runtime 的全局对象。.m和.c只有在模块代码生成明确要求对应 capability 时才可见;工厂项只返回不可调用的占位包装,缓存项只返回不可替换exports的冻结快照,不暴露原始工厂或 module record。.t等可能间接加载模块的 helper 会再次校验目标 ID。.i、未知 helper、constructor、prototype、__proto__、构造调用以及属性写入、删除和defineProperty都会被拒绝。- 其余函数型 helper 通过冻结包装调用,对象型 helper 通过递归只读视图暴露;
call、apply和bind仍可使用,但调用的仍是受保护 require。
拒绝时抛出的错误带有固定错误码:
error.code === "WARDEN_WEBPACK_REQUIRE_DENIED";错误同时包含调用方 ID、目标 ID 和可解析的模块路径;开发模式还会附带当前允许列表。
人工追加 require 权限
自动依赖以外的精确授权写入 policy-over.json:
{
"src/example.ts": {
"webpackRequire": {
"allow": ["node_modules/example/index.js"]
}
}
}路径必须是相对于项目根目录的 POSIX 精确路径,不支持 glob。调用方或目标不存在、目标存在歧义,或者两者不属于同一 runtime 时,构建会失败。
顶层的 enable: false 只关闭该模块的全局 Proxy 沙箱,不会关闭 require 保护。require 保护只能通过插件级 protectWebpackRequire: false 全局关闭。
三、关键原型冻结
开启 freezePrototypes 后,Warden 会发射同源的 warden-prototype-freeze.js,并保证它先于策略 runtime 和业务脚本执行。
当前冻结范围包括:
Boolean、Number、BigInt、String、Symbol。Array、RegExp、Promise.prototype、Map、Set、WeakMap、WeakSet。Error.prototype、AggregateError、SuppressedError及全部原生错误子类;compat 模式保留 V8 调试钩子Error.stackTraceLimit和Error.prepareStackTrace可修改。DisposableStack、AsyncDisposableStack(环境存在时)。WeakRef、FinalizationRegistry、Iterator(环境存在时)。ArrayBuffer、SharedArrayBuffer、DataView和全部具体 typed array 构造器及其原型。Atomics、Intl命名空间,以及环境提供的全部Intl构造器及其原型。- 环境存在时的
Crypto、CryptoKey、SubtleCrypto、TextEncoder、TextDecoder。 Proxy、JSON、Math。
保护清单集中定义在 src/prototype-protection.ts。测试会校验沙箱默认放行的每个 ECMAScript 全局都在清单中被明确分类,新增全局但遗漏保护策略时会导致测试失败。
compat 和 strict 模式都会冻结 crypto.subtle 单例及其实际原型,阻止对 digest、encrypt 等 Web Crypto 方法进行实例遮蔽、重定义或删除。
同时,默认的 compat 模式为兼容现有生态保留以下扩展点:
Object、Object.prototype、Function、Function.prototype、Date和Date.prototype不会被整体冻结,但它们启动时已经存在的成员会被锁定;仍允许增加新成员,也允许派生对象创建同名自有属性。Function.prototype.toString是既有成员中的明确例外,保留监控 SDK 改写并恢复它的能力。Reflect对象,允许reflect-metadata添加新 API。Promise构造器,兼容 Dexie 等库临时替换并恢复 Promise 静态方法;Promise.prototype仍冻结。- typed array 共享构造器和共享原型保持可扩展,但其既有静态方法和原型方法会被锁定;
Buffer等子类仍可定义自己的from、toString、includes、slice。
锁定方式允许派生函数或实例创建同名自有属性,避免破坏 fn.call = value、obj.toString = value 或 Buffer 子类覆盖 inherited typed-array 方法一类代码。
prototypeFreezeMode: "strict" 会进一步深度冻结 Object、Function、Date、Promise 构造器及其原型、共享 TypedArray 祖先、迭代器/生成器/异步生成器等隐藏 intrinsic 原型,以及原型方法函数和 Array.prototype[Symbol.unscopables] 一类嵌套对象。compat 模式仍保留前述生态兼容扩展点。
直接修改已锁定目标会抛出明确错误,例如:
TypeError: [Warden] Modification of locked target "Reflect.get" is not allowed可选 API 不存在时会跳过;对已经存在的目标,如果锁定或结果校验失败,bootstrap 会停止业务启动并报告具体目标。
运行时资产与加载顺序
| 运行环境 | 加载方式 |
| --- | --- |
| 普通 HTML / MV2 background page | 在所有 HTML 的 <head> 首位同步插入 warden-prototype-freeze.js(若开启)、warden-policy-runtime.js、业务脚本 |
| MV3 service worker | 从 manifest.json 识别 background.service_worker,在业务代码前执行冻结 bootstrap,再 importScripts("warden-policy-runtime.js") |
| manifest content script(默认隔离世界) | 策略封装在 runtime 闭包中,并在脚本首行执行原型冻结 bootstrap |
| world: "MAIN" content script / pageProvider.js | 策略封装在各自 runtime 闭包中,不向网页 MAIN world 注入原型冻结脚本 |
所有 HTML 注入均使用外部同源脚本,不生成内联代码,适配浏览器扩展 CSP。嵌套 HTML 会根据 publicPath 或相对目录生成正确引用。
外部页面和 Worker 共用的策略数据保存在 warden-policy-runtime.js。该文件使用路径分段 Trie、去重的依赖集合和紧凑 ID 表减少重复数据,并在入口模块执行前完成结构校验和冻结;它只提供不可变策略记录和诊断,不提供创建沙箱的能力。真正的沙箱工厂位于各 Webpack runtime 的闭包中。content script 与 pageProvider.js 则把策略记录内联到各自的 runtime 闭包,避免依赖页面 MAIN world 的共享策略。
外部 runtime 会提供只读诊断对象:
__wardenDiagnostics.resolvePolicyPath(policyId);
__wardenDiagnostics.resolveWebpackModulePath(moduleId);诊断对象、只读策略解析器和内部策略记录均不可写、不可配置,并在创建后冻结。策略解析器返回的只是不可变元数据;__mkGb 不再存在于全局对象上。
Policy 文件
构建产物 policy.json
policy.json 是便于审计的最终有效策略,而不是运行时存储格式:
{
"node_modules/example/index.js": {
"enable": true,
"require": [
"node_modules/example/dependency.js"
],
"global": {
"chrome": "r",
"navigator": "rw"
}
},
"node_modules/legacy/index.js": {
"enable": false
}
}enable表示该模块是否启用全局 Proxy 沙箱。require是模块运行时最终允许加载的模块路径,包括自动依赖与人工追加权限。global是模块最终允许访问的全局变量及权限。- 空的
require、global会被省略。 - 如果一个模块最终只有
"enable": true,整个模块条目会被省略;"enable": false始终保留。
动态属性名无法完整表示为静态 global 字段,但运行时通过 Proxy 发起的动态访问仍会按实际属性名校验。构建不会为这类访问打印 warning。
三方模块依赖边界
Warden 会检查最终 policy.json 中的 require 权限:如果来源模块路径以 node_modules/ 开头,而它允许加载的目标模块路径既不以 node_modules/ 开头,也不以 webpack: 开头,且目标文件名不是 package.json,则视为三方模块引用一方代码。webpack: 表示 Webpack 生成或忽略的内部模块,package.json 只提供包元数据,二者都属于安全目标。自动依赖和 policy-over.json 人工追加的 require 权限使用同一检查口径。
开发模式默认逐条输出 console.warn;生产模式(dev: false)默认汇总全部越界依赖并终止构建。可通过显式 forceCheck 覆盖默认值。该检查依赖 protectWebpackRequire 生成最终 require 策略;关闭 require 保护后,policy.json 不包含相应依赖关系,因此不会执行这项边界判断。
敏感全局检查 check.json
如果 Webpack context 下存在 SecSDK/check.json,Warden 会在构建阶段检查最终 policy.json 中的全局权限。配置的键是敏感全局名,值是允许获得该权限的模块路径正则数组:
{
"document": [
"^node_modules/react-dom",
"^node_modules/antd"
],
"chrome": [
"^node_modules/webextension-polyfill"
],
"fetch": []
}模块路径与 policy.json 的键一致,使用相对于 Webpack context 的 POSIX 路径。一个模块只要命中对应全局名下的任一 JavaScript 正则表达式即可通过;空数组表示不允许任何模块获得该全局权限。r 与 rw 使用同一套模块白名单,人工 policy-over.json 追加的权限也会参与检查。未在 check.json 中声明的全局名不受这项构建检查影响。
开发模式默认对每个违规的“模块 + 全局名”输出一条 console.warn;生产模式(dev: false)默认汇总全部违规并终止构建。可显式设置 forceCheck 覆盖默认行为:
new WardenPlugin({
forceCheck: true,
});check.json 不存在时会跳过检测;文件存在但 JSON 结构、数组项或正则表达式无效时始终终止构建。
人工策略 policy-over.json
{
"node_modules/example/index.js": {
"enable": true,
"global": {
"allow": {
"chrome": "r",
"navigator": "rw"
}
},
"webpackRequire": {
"allow": [
"node_modules/example/dependency.js"
]
}
}
}人工 global.allow 会覆盖同名自动检测权限,webpackRequire.allow 会追加到依赖图授权。所有模块路径都使用相对于 Webpack context 的 POSIX 路径。
安全边界
使用 Warden 时需要注意以下边界:
- 全局 Proxy 沙箱目前只改写
node_modules模块;应用源码不会自动进入该层沙箱,但仍受 Webpack require 保护。 - 静态可确定的裸全局标识符按设计保持原生词法访问,不经过 Proxy;构建期分析与
check.json负责审计这类能力。动态生成代码、未被解析器看到的代码和提前保存的真实引用同样不在 Proxy 保证内。 - Symbol 属性当前默认允许读写,以保持宿主对象和运行时协议兼容。
- Webpack runtime、RuntimeModule 和 chunk loader 是受信任基础;保护对象是传给业务模块工厂的 require。
- 未进入全局 Proxy 沙箱的 concatenated factory 内部源码模块共享一个 require 授权单元,Warden 不隔离同一工厂内部的直接符号访问;受全局 Proxy 保护的第三方模块会主动退出 concatenation。
- 默认
prototypeFreezeMode: "compat"下Object.prototype仍可扩展,因此不等于消除全部普通对象原型污染路径;需要阻断这一路径时使用"strict"。 - Warden 不冻结应用内部的 Keyring、Buffer 或其他业务类,也不替代私钥材料的对象级生命周期管理。
- manifest 默认隔离世界的 content script 会执行关键原型冻结;显式
world: "MAIN"的 content script 和pageProvider.js不冻结网页原型,避免改变被访问页面的 JavaScript 行为。 - 能进行动态代码生成或已提前获得真实对象引用的代码,不应被当作完全不可信的任意 JavaScript 放入当前沙箱;需要这种隔离时应使用独立 Realm、Worker 或进程边界。
源码架构与维护
包根入口保持精简,Webpack 编译期逻辑与生成到浏览器中的运行时代码分开维护:
| 模块 | 职责 |
| --- | --- |
| src/index.ts | 公共入口,只导出 WardenPlugin 和 WardenPluginOptions |
| src/plugin/warden-plugin.ts | 初始化各协作模块并注册 Webpack 生命周期 hook |
| src/plugin/module-policy.ts | 识别需要沙箱化的模块、选择分析源码、合并全局权限并改写模块工厂 |
| src/plugin/webpack-module-graph.ts | 解析模块路径、active dependency、runtime requirement 和 concatenated module |
| src/plugin/webpack-policy.ts | 生成每个 runtime 的 require/global 策略,处理人工授权并汇总审计记录 |
| src/plugin/policy-files.ts | 读取人工策略和检查规则,生成、校验并稳定写入 policy.json |
| src/plugin/runtime-assets.ts | 发射冻结/策略资产,并处理 HTML、MV2、MV3 的加载顺序 |
| src/plugin/contracts.ts | 插件选项、编译期状态和内部策略结构等共享契约 |
| src/globals.ts、src/global/ | 保留原有深层入口,同时分别维护能力清单、AST 分析、bind 改写和 Proxy 沙箱 |
一次编译的主要数据流是:
模块源码分析
↓
全局权限与 Webpack 依赖策略生成
↓
人工策略合并、敏感权限与依赖边界审计
↓
policy.json 写入、runtime 资产发射与页面/Worker 注入维护生成代码时需要遵守以下约束:
mkCompartmentFactory会通过toString()写入 Webpack runtime,函数体必须自包含,不能依赖模块闭包中的常量或辅助函数。- 策略中的模块路径统一为相对于 Webpack
context的 POSIX 路径;生成模块使用稳定的webpack:标识。 - 不要随意调整 Webpack hook 或
processAssetsstage。模块分析、runtime policy 生成、资产发射和最终注入依赖现有先后顺序。 src/index.ts和src/globals.ts是兼容入口。内部实现可以继续拆分,但包根导出及已有深层模块导出应保持稳定。
开发与验证
npm run check
npm testnpm run check 只执行 TypeScript 类型检查;npm test 会先构建 dist/,再运行全部 Node.js 测试。
测试覆盖全局 Proxy 反射陷阱、关键原型兼容性、策略 runtime 校验、Webpack 同步与异步依赖、context、concatenation、HMR、require 旁路以及 HTML/MV2/MV3 资产加载顺序。test/exports.test.js 额外锁定包根和已有深层模块的运行时导出,防止内部模块化意外变成破坏性 API 变更。
