extshield
v1.0.2
Published
Minify and harden Chrome extensions without breaking Web Store policy — esbuild minification, terser property mangling, WASM sinking and a pre-upload compliance scan.
Maintainers
Readme
extshield — Chrome 扩展压缩合规加固工具
🌐 English | 简体中文
在不踩 Chrome Web Store 审核红线的前提下,尽量提高扩展被逆向复刻的门槛。
激进压缩(esbuild)+ 属性改名(terser)+ WASM 下沉 + 上传前合规扫描,附带一个本地可视化打包器。
目录
作用
Chrome Web Store 政策明确禁止 obfuscation(混淆),但允许 minification(压缩)。
| 手段 | 是否允许 | 说明 |
|------|----------|------|
| 去空白/注释、缩短变量名、合并文件 | ✅ 允许 | 这就是 minification |
| 属性名改名(terser mangle) | ✅ 允许(需谨慎) | 更激进,可能破坏跨文件调用 |
| 字符串加密 / 控制流平坦化 / eval(解密代码) | ❌ 禁止 | 直接判定为混淆,审核拒绝 |
真正能称得上"代码保护"的合规路径只有两条:
- 把核心逻辑下沉到服务端 —— 代码根本不进客户端(扩展只做请求+展示),他人拿不到;
- 把关键计算编译成 WASM —— 代码在客户端,但天然难逆向,且不属于 JS 混淆。
两条性质不同:一条是"让他人拿不到代码",一条是"拿到了也难读"。
本工具解决的是第 0 步:用合规手段把 JS 压到"能读但很难读",并在上传前
用 verify 自动拦住一切会踩红线的苗头——避免你"阴差阳错"把违规代码传上去。
安装
环境要求:Node.js ≥ 18(用到 fs.rmSync 等较新 API;Windows / macOS / Linux 均可)。
npm install -g extshield或克隆本仓库后:
cd extshield
npm install # 依赖:esbuild / terser / acorn / assemblyscript全局安装后命令为 extshield;在克隆的仓库里,用 node bin/extshield.js(或 npx extshield)加同样的子命令。
使用
extshield 提供两种使用方式:可视化打包器(本地网页界面)与命令行。首次使用建议从可视化打包器开始 —— 它把探测到的入口、体积对比与合规报告集中在一页里展示,无需记忆任何参数;命令行则用于脚本与 CI。
1) 加固(harden)
# 用当前目录的 extshield.config.js
extshield harden
# 指定目录 / 开启属性改名
extshield harden --src ./src --out ./dist --mangle-props做了什么:
- 从
manifest.json自动探测入口(background / content / popup / options); - 用 esbuild 做 bundle + 激进压缩(去空白、改名、tree-shaking、去
console/debugger、去注释); - 拷贝 manifest / html / css / 图片等静态资源;
- 可选:terser 属性名改名(
--mangle-props)。
2) 合规扫描(verify)
extshield verify --dir ./dist
extshield verify --dir ./dist --strict # CI 卡口:中等风险也判不通过会扫描产物,识别这些高危/中等模式并报告:
eval()、new Function()、setTimeout 传字符串、atob/btoa 解密链、
fromCharCode 解码、长 base64 字符串表、_0x 混淆器变量名、控制流平坦化、
constructor.constructor 逃逸、sourceMappingURL 残留、远程代码加载等。
verify 退出码:通过 0,存在高危项 1(strict 下含中等项)。可直接接 CI。
3) 一键演示(demo)
extshield demo用内置 sample/ 扩展执行一遍 harden + verify,验证工具是否可用。
加 --wasm 可一并查看 WASM 下沉的效果:
extshield demo --wasm4) WASM 下沉(--wasm)
把"纯计算"的函数编译进 WebAssembly。产物里是 wasm 字节码,想读懂需先反汇编 —— 复刻门槛明显高于纯 JS。它是 Chrome 政策允许的(属于编译产物,而非加密),审核能过。
下沉的同时,工具会把 wasm 里的函数导出名抹成 f0 / f1:否则产物里会原样写着
(export "isSimilarUrl" ...),等于白送逆向者一套"哪个函数值钱"的路标。抹名不影响任何
功能 —— memory、__new 等运行时接口会原样保留,只是不再暴露你自己的函数名。
# 自动下沉:由工具扫描你的源码,挑出适合的函数
extshield harden --src ./src --out ./dist --wasm
# 手动下沉:使用你自己编写的 core.ts(优先级更高)
extshield harden --src ./src --out ./dist --wasm --wasm-core ./core.ts两种方式的区别:
| | 自动下沉(默认) | 手动下沉(--wasm-core) |
|---|---|---|
| 你要做什么 | 无需任何操作 | 自行编写 core.ts |
| 挑哪些函数 | 工具按规则扫描:只做数值运算、不涉及浏览器 API 的 | 由你决定 |
| 产物形态 | wasm 以 base64 内联进 JS,同步调用 | 同样是内联(不产出 core.wasm / loader) |
| 要不要改 manifest | 无需修改 | 无需修改(但工具会自动补上 CSP 放行,见下) |
| 调用点 | 无需改动 | 无需改动(同名函数自动替换) |
| 适合 | 想快速见效 / 不想接触 wasm | 想把核心算法掌握在自己手中 |
手动下沉以前会产出独立的
core.wasm+wasm-loader.js(需要web_accessible_resources、 调用点还得改成await)。现在改成和自动下沉一样内联:同步实例化,调用点无需改动, 也不会多出可在chrome-extension://直接下载的 wasm 文件。
⚠️ 内联 wasm 之后,必须放行 CSP(必读)
这是实测遇到过的问题:MV3 扩展页默认 CSP 是 script-src 'self',而编译 WebAssembly 被
CSP 当成代码求值,于是内联进 popup / service worker 的 wasm 会被直接拦下,报:
CompileError: WebAssembly.Module(): ... violates the following CSP directive: "script-src 'self'"工具现在会自动补上这段(只追加、不覆盖你原有的指令,重复执行不会重复改写):
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
}三条硬约束(已核对 Chrome 官方文档):
script-src/object-src/worker-src只允许self/none/wasm-unsafe-eval。 写'unsafe-eval'会让扩展安装直接失败,绝不可为图省事而加上。'wasm-unsafe-eval'只放行 wasm、不放行eval,Chrome 商店接受。- 用的是隔离世界的
content script?无需处理 CSP —— Chrome 给隔离世界的默认 CSP 本来就带'wasm-unsafe-eval',script-src 'self' 'wasm-unsafe-eval' ...,wasm 能正常跑。 但显式写了"world": "MAIN"的 content script 会注入网页主世界,套用网页自己的 CSP, 严格站点会拦掉 wasm —— 所以工具只把这类文件排除在下沉之外,隔离世界的照常下沉。 (用chrome.scripting.executeScript({world:'MAIN'})动态注入的脚本不在 manifest 里, 工具看不到,需要自行避开。)
参考:https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts
几个要提前知道的点:
- 自动下沉不会改动你的源码。它会先把源码复制一份到临时目录,在副本上修改,
产物从副本打包;你的工程目录不会被改动。如需保留副本检查,加
--keep-stage。 - 不是所有函数都能沉。涉及
chrome.*/document/fetch,或者用了 字符串、数组、对象的函数会被跳过 —— 这些无法在 wasm 里运行。跳过时工具会 逐个打印跳过原因,不会静默忽略。 - 没有可下沉的函数时,不会以内置示例 wasm 替代。报告会说明扫描的函数总数, 以及各函数未能入选的原因;产物中不含 wasm。如此处理是为了避免造成 "包含 core.wasm 即表示已受保护"的误判。
- 它只对"有纯计算逻辑"的扩展有用。如果你的插件主要是 UI 和 DOM 操作 (像绝大多数弹窗类插件),可能一个函数都无法下沉 —— 这属于正常情况, 此时起作用的只有压缩 + 合规扫描,不要指望 WASM。
- 想让更多函数可下沉,就把"纯计算"部分拆成独立函数,避免与 DOM / 存储操作混写。
配置文件里也可以开启:wasm: true / wasmCore: './core.ts'。
配置文件 extshield.config.js
放到项目根目录即可被自动读取,字段全可选,见 templates/extshield.config.js。
防范程度
- ✅ 可逆但费力:minify + 改名后,源码逻辑仍在,只是变量名变成
a/b/c、 结构被打平。逆向者需要额外的时间成本,但无法 100% 阻挡有决心的逆向者。 - 🧱 成本更高,但同样可逆:
--wasm把纯计算函数编译成 wasm 字节码,并抹掉函数 导出名(不再出现(export "isSimilarUrl" ...)这种一读就懂的路标)。经实测 (binaryen 反汇编):wasm 不是加密 —— 函数体、常量、控制流仍可还原,只是读起来 远比 JS 费劲。定位是抬高成本,不是封锁。 CLI 加--wasm即可(默认按规则自动下沉,也可用--wasm-core指定自己的core.ts); GUI 里勾 WASM 是同一套逻辑(CLI / GUI 共用src/wasm-sink.js)。wasm-template/另有一份独立的 AssemblyScript 示例 + 编译脚本 + loader, 适合希望手工走传统"单独core.wasm文件"路线的人,和工具的内联方案互不影响。 - 🚫 客户端不会有"不可逆":代码要在客户端跑,就必须能被读、被分析。加密 + 运行时 解密属于 MV3 明文禁止的混淆手段,本工具刻意不做。
- 🔒 唯一真正"拿不到"的路:核心算法不下发到客户端,改由服务端 API 计算。 代价是服务器成本、必须联网、而且要把用户数据传出去 —— 所以它只适用于本来就该在 服务端的逻辑(授权校验、云端数据、需要密钥的调用);读 DOM / 改页面这类纯前端功能 天生沉不下去。这里列出来是为了讲清边界,不是建议所有项目都这么做。
- ❌ 不提供:字符串加密、控制流平坦化、反调试——这些是政策明文禁止的,本工具 刻意不做,因为那会导致上架失败。
CI 集成示例(GitHub Actions)
- name: 加固并校验
run: |
npm ci
npx extshield harden
npx extshield verify --dir ./dist --strict可视化打包器(GUI)
不想敲命令?一条命令启动本地 Web 应用:选文件夹 → 选方式 → 选保存位置 → 一键合规打包, 打包完成后同时在页面上给出合规扫描报告。
extshield gui # 打开 http://localhost:4173
extshield gui --port 8080 # 换一个端口在克隆的仓库里,npm run gui 启动的是同一个界面。
两种方式(minify / wasm)都做成可视化按钮,详见 gui/README.md。
界面支持中英文切换(右上角 中文 / EN),默认跟随浏览器语言 —— 中文浏览器显示中文、其余显示英文,
选择会记住;也可以用 http://localhost:4173/?lang=zh(或 ?lang=en)直接指定语言。
服务只绑定 127.0.0.1,并校验 Host / Origin / Sec-Fetch-Site,阻断 DNS rebinding 与跨站请求。
目录结构
extshield/
├── bin/extshield.js CLI 入口(harden / verify / demo / gui)
├── src/
│ ├── harden.js esbuild 打包 + 激进压缩(+ 可选 terser 属性改名)
│ ├── verify.js scan() 纯函数扫描 + run() CLI 包装(退出码)
│ ├── rules.js 合规规则库(高危 / 中等 / 提示)
│ ├── config.js DEFAULTS + 从 manifest 自动探测入口
│ ├── auto-sink.js 自动下沉引擎(acorn 扫 AST 选函数 → AssemblyScript)
│ ├── manual-sink.js 手动下沉(编译你的 core.ts,替换同名函数)
│ ├── wasm-sink.js CLI / GUI 共用的下沉编排(prepare / finalize)
│ ├── wasm-rename.js 抹掉 wasm 的函数导出名(保留运行时接口)
│ ├── zip.js 纯 Node 打 zip(不依赖外部命令)
│ └── asc.js / runtime-gen.js / wasm-loader-gen.js 编译与运行时生成
├── gui/ 本地可视化打包器(server.js + public/,界面中英双语)
├── templates/ 配置文件模板
├── wasm-template/ 独立的 AssemblyScript 示例(可选,见下)
├── sample/ 内置示例扩展(demo 用)
└── test/e2e-wasm.js 端到端回归(npm test)
wasm-template/是给"想自己写 wasm、不使用工具的自动下沉"的人准备的独立模板: 它编译出单独的core.wasm+wasm-loader.js(需要web_accessible_resources), 和本工具的内联方案不同,按需取用。
开发与测试
npm test # 端到端回归:自动下沉 / 手动下沉 / 字符串语义 / CSP / 边界安全test/e2e-wasm.js 会实际启动一次 GUI 打包服务(随机端口)、实际编译一次 wasm、再解出产物并实际运行,验证"下沉后与原 JS 输出一致"——而非仅检查文件是否存在。
隐私说明
- GUI 模式:你上传的扩展源码会临时写入
gui/.work/<任务ID>/用于处理, 打包完成后立即删除,不在磁盘留存;服务启动时也会清空一次gui/.work/, 兜底清理上次异常退出(强杀 / 断电)留下的副本。 - CLI 模式:全程只读写你指定的
--src/--out目录;开启--wasm时会额外在系统 临时目录建一份源码副本(下沉必须改文件,不能在你的工程目录中就地修改),运行结束后自动删除, 加--keep-stage可保留下来检查。 - 两种模式都不联网,不会把源码发往任何外部服务器。
- 建议:不要把
gui/.work/纳入自己的备份或同步范围。
免责声明
- 本工具只做 Chrome Web Store 政策允许的压缩与 WASM 下沉,不提供任何被禁止的 混淆手段。能否通过审核最终由 Chrome Web Store 判定,本工具不对此作保证。
- 压缩 / 属性改名可能破坏依赖原变量名或属性名的代码(跨文件调用、外部 API、
消息键等)。请先在小范围验证,并用
verify复检。 - 产物请自行做好版本管理与备份,使用本工具产生的一切后果由使用者自负。
第三方依赖与许可
本项目自身以 MIT 发布;用到的第三方组件共 16 个(直接依赖 4 个、传递依赖 12 个), 全部是宽松许可(MIT / BSD-2-Clause / BSD-3-Clause / Apache-2.0),不含 GPL / AGPL / LGPL 等传染性许可,因此不影响本项目继续以 MIT 发布,也不影响你对自己产出的扩展包自行授权。
各组件的版本、许可与版权归属见 THIRD-PARTY-NOTICES.md —— 它由
scripts/gen-notices.js 直接从实际安装的依赖中生成(不联网、不推测),依赖变动后运行
npm run notices 重新生成。
三点值得留意:
- 只是通过 npm 安装使用:无需额外动作。每个依赖包自带自己的
LICENSE,由它自己满足 "保留版权声明"的义务;声明文件的作用是可读与审计(企业内部做开源许可审查时常被要求提供)。 - 如果你把依赖打包进发布物(离线包 / 单文件 / 免安装发行):则必须同时附上各组件的
许可全文,Apache-2.0 组件还需保留其
NOTICE。声明文件末尾给出了可直接使用的生成命令。 - 启用
--wasm时:产出的 wasm 二进制里内联了 AssemblyScript 的运行时(它派生自 TypeScript / Binaryen / musl libc / V8 / Arm Optimized Routines),按 Apache-2.0 建议随产物 保留其归属声明 —— 摘要见声明文件第四节。
许可证
本项目采用 MIT 许可证,详见 LICENSE。
