从微信小程序到全端:一个安全扫描器的演进之路(v0.5.0 → v0.6.0)
上一篇讲了"从 0 到 GitHub Marketplace",这一篇讲"从专用到通用"。一个只能扫微信原生小程序的工具,如何通过 3 次技术迭代,支持 Taro、uni-app、React、Vue,最终变成一个"全端前端安全扫描器"。
项目地址:
- GitHub: github.com/lathuninh/m…
- PyPI: pypi.org/project/min…
- Marketplace: github.com/marketplace…
一、起点:v0.5.0 的能力边界
v0.5.0 发布时,工具能扫:
- ✅ 微信原生小程序(
wx.request/e.detail.value/wx.setStorageSync) - ✅ 普通 Web 应用(
location.search/innerHTML)
但扫不了:
- ❌ Taro(
Taro.request/export default) - ❌ uni-app(
uni.request) - ❌ React 组件(
export default function App()) - ❌ Vue 单文件组件(
.vue文件的<script>)
这 4 个场景,覆盖了 80% 的中国前端项目。
决定:把工具从"微信专用"扩展成"全端通用"。
二、第一次迭代:Taro 支持
2.1 加规则:Taro API 到 taint.yaml
Taro 的 API 和微信原生几乎一样(Taro.request vs wx.request)——只要把 Taro 的 API 加到 sink 和 source 列表:
sinks:
- name: "Taro.request"
dangerous_arg: [0, 1]
severity: medium
message: "Tainted data into Taro.request"
- name: "Taro.setStorageSync"
dangerous_arg: [1]
severity: high
sources:
- member: "Taro.getStorageSync"
- member: "Taro.getStorage"
这一步很简单。
2.2 加项目类型识别
_detect_project_type 读 package.json:
python
if (root / "package.json").exists():
try:
import json
pkg = json.loads((root / "package.json").read_text(encoding="utf-8-sig"))
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
if "@tarojs/taro" in deps:
return "taro"
if "@dcloudio/uni-app" in deps:
return "uni-app"
except Exception:
pass
2.3 🐛 踩坑 1:PowerShell 写 package.json 带 BOM
用 Set-Content -Encoding UTF8 写的文件带 BOM(开头几个不可见字节)——json.loads 读到就报错:
text
json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig)
修复:utf-8 → utf-8-sig(自动去 BOM)。
2.4 🐛 踩坑 2:export default 没被处理
Taro 项目用 export default:
javascript
export default function Pay() {
const handlePay = (e) => {
const amount = e.detail.value.amount;
Taro.request({ url: '...' + amount });
};
}
_exec_stmt 只处理 FunctionDeclaration——但 export default function Pay() 的 AST 是 ExportDefaultDeclaration——Pay 函数体根本没被执行——所以 Taro.request 没扫到。
修复:在 _exec_stmt 里加 ExportDefaultDeclaration 和 ExportNamedDeclaration:
python
elif t == "ExportDefaultDeclaration":
decl = node.get("declaration", {})
dt = decl.get("type")
if dt == "FunctionDeclaration":
self._handle_function_like(decl, decl.get("id", {}).get("name", ""), file_path)
elif dt in ("ArrowFunctionExpression", "FunctionExpression"):
self._handle_function_like(decl, "__default__", file_path)
elif t == "ExportNamedDeclaration":
decl = node.get("declaration")
if decl:
self._exec_stmt(decl, file_path)
这个 bug 影响很大——不只是 Taro——所有用 ES Module 的项目(React / Vue / 现代前端)都受影响。
2.5 🐛 踩坑 3: _handle_var_decl 的缩进错误
const handlePay = (e) => { ... } 是 VariableDeclaration——需要处理 ArrowFunctionExpression——但你加 elif 时缩进错了:
python
elif it == "MemberExpression":
obj = self._member_name(init.get("object"))
prop = self._member_name(init.get("property"))
if obj and prop and obj != "this":
self.aliases.record_prop_alias(name, obj, prop)
elif it == "ObjectExpression": # ← 错误!这个 elif 缩进太深了
...
ObjectExpression 和 FunctionExpression 的 elif 被错误地嵌套在了 if it == "MemberExpression" 里面——永远进不去。
修复:统一缩进:
python
elif it == "MemberExpression":
obj = self._member_name(init.get("object"))
prop = self._member_name(init.get("property"))
if obj and prop and obj != "this":
self.aliases.record_prop_alias(name, obj, prop)
elif it == "ObjectExpression": # ← 正确:和 if 平级
...
elif it in ("FunctionExpression", "ArrowFunctionExpression"):
self._handle_function_like(init, name, file_path)
2.6 结果
修复后——Taro 100% 支持——并且顺手修好了 export default(顺便覆盖 React / Vue)。
三、第二次迭代:uni-app 支持
uni-app 的 API 更简单——uni.request / uni.setStorageSync——只要加规则:
yaml
sinks:
- name: "uni.request"
dangerous_arg: [0, 1]
severity: medium
- name: "uni.setStorageSync"
dangerous_arg: [1]
severity: high
sources:
- member: "uni.getStorageSync"
- member: "uni.getStorage"
10 分钟搞定——因为项目类型识别已经在 Taro 那次做好了。
四、修复 _resolve 单层路径(重要)
4.1 问题
你的 DSL 规则用了 match: [value, ...] ——但 value 被当字面量返回——导致规则不命中。
4.2 根因
_resolve 方法:
python
if "." not in path:
return path # ← 单层路径一律当字面量
"value" 不含 . ——返回字符串 "value" ——而不是 ctx.node["value"] 。
4.3 修复
python
if "." not in path:
# 单层路径:先尝试从 node 取
if isinstance(ctx.node, dict) and path in ctx.node:
return ctx.node[path]
# 否则当字面量(比如 "eval"、"debug")
return path
这个修复让 DSL 规则表达能力提升了一个档次——可以写 eq: [value, "..."] 这种细粒度规则。
五、新增规则:信息泄露(补天场景)
加了 3 条"补天场景"规则:
- DSL-060:API 路径泄露(
/api/admin/xxx//api/internal/xxx) - DSL-061:Debug 模式开启(
debug: true) - DSL-062:测试凭据硬编码(
test_user: "admin")
这些规则能帮你在补天上快速发现"信息泄露"线索。
六、踩过的所有坑(完整清单)
| 坑 | 原因 | 修复 |
|---|---|---|
export default 没扫到 | AST 是 ExportDefaultDeclaration | 加节点处理 |
package.json BOM 报错 | Set-Content 写文件带 BOM | 用 utf-8-sig |
elif 缩进错 | 粘贴时缩进多了 | 统一缩进 |
_resolve 单层路径 | 不含 . 一律当字面量 | 先查 ctx.node |
Taro.request 不命中 | ObjectExpression 没返回污点 | 修 _handle_var_decl |
editable install 缓存 | 改代码不生效 | --force-reinstall |
| CLI 编码崩溃 | GBK 不支持 emoji | colorama.just_fix_windows_console |
每一个坑都值得写一篇文章。
七、数据:v0.5.0 → v0.6.0
| 指标 | v0.5.0 | v0.6.0 | 变化 |
|---|---|---|---|
| 支持框架 | 微信 / Web | 微信 / Taro / uni-app / React / Vue / Web | +4 |
| 规则数 | 50 | 55+ | +5 |
| 污点 sink | 15 | 23 | +8 |
| 污点 source | 25 | 35 | +10 |
| 测试 | 25 | 25(全绿) | 不变 |
| Commits | 37 | 42+ | +5 |
核心变化:从"微信专用"到"全端通用"。
八、为什么这件事很重要
前端生态是"多框架"的:
- 小程序:微信原生 / Taro / uni-app
- Web:React / Vue / Angular / Svelte
一个只支持"微信原生"的工具——只能覆盖 20% 的项目。
一个支持"全端"的工具——能覆盖 80% 的项目。
从 20% 到 80% ——只需要加几个 API 规则 + 修复 export default——ROI 极高。
九、下一步
继续优化:
- 支持 Vue SFC 完整解析(
.vue文件的<template>) - 支持 React JSX 解析
- 加更多规则(越权 / SDK 滥用 / CSRF)
让工具从"能扫"到"扫得准"。
十、开源地址
- GitHub: github.com/lathuninh/m…
- PyPI: pypi.org/project/min…
- Marketplace: github.com/marketplace…
- 第一篇: 从零构建
- 第二篇: 深入污点分析
- 第三篇: 从 0 到 Marketplace
欢迎 Star / Issue / PR。
十一、总结
从 v0.5.0 到 v0.6.0——核心是"三个修复 + 两个规则" :
- 修
export default(覆盖 React / Vue / Taro) - 修
_resolve单层路径(提升 DSL 表达能力) - 修
_handle_var_decl缩进(修复箭头函数处理) - 加 Taro API 规则(支持 Taro)
- 加 uni-app API 规则(支持 uni-app)
每一个修复都让工具更通用——每一个规则都让工具更精准。
开源项目的演进——不是一次写完——是持续迭代——每次解决一个真实问题。
加油! 大家加油,努力复盘总结🚀