微信小程序安全实战:抓包与反编译交叉审计指南
1. 这不是“黑产教程”,而是一线小程序安全工程师的日常拆解现场
微信小程序上线前的安全验收,从来不是点开开发者工具看一眼console就完事。我做过27个金融类、12个政务类、8个医疗健康类小程序的安全评估,几乎每次都会在第一轮测试里发现:某个支付回调接口没做签名校验、某个用户token被明文拼在wx.request的url里、某个敏感字段在wxml模板中直接用{{userInfo.phone}}渲染——而这个phone字段,后端根本没做脱敏,前端也没加掩码逻辑。这些不是理论漏洞,是真实存在于已上线生产环境里的“裸奔”细节。微信小程序安全攻防:从抓包到反编译的实战指南,说的正是我们每天在灰盒测试中反复操作的那套动作链:先用抓包确认流量是否可控,再用反编译验证代码是否可逆,最后回归业务逻辑判断风险是否真实存在。它不教你怎么入侵别人的小程序,而是告诉你——当你的小程序被别人这样测时,哪些地方会最先露馅;当你自己要上新功能时,哪些写法会在30秒内被逆向出核心逻辑。适合两类人:一是刚接手小程序安全工作的开发/测试同学,需要一套可立即上手的验证路径;二是资深前端或全栈工程师,想补全客户端侧安全闭环的最后一环。这不是CTF题库,没有花哨的0day利用,只有真实环境里最常踩的坑、最稳的验证方式、以及改一行代码就能堵住的缝隙。
2. 抓包不是为了“看流量”,而是为了定位“谁在替你做决策”
很多人把抓包理解成“看看请求长啥样”,这完全低估了它的价值。在小程序安全场景中,抓包的核心目的只有一个:识别并剥离所有本该由客户端承担、却实际交由服务端代劳的安全责任。比如,一个“获取用户优惠券列表”的接口,如果请求里只带了一个加密后的openId,但响应体里直接返回了每张券的过期时间、使用门槛、核销码明文——这就意味着:券的有效性校验、防刷逻辑、甚至核销权限,全部压在了服务端。而一旦攻击者拿到这个接口的调用方式,他不需要破解任何加密,只要批量请求+解析响应,就能自动化薅走所有可领取的券。这才是抓包要揪出来的真问题。
2.1 小程序抓包的三个不可绕过前提
第一,必须关闭“HTTPS证书校验”。小程序默认强制校验SSL证书,这意味着如果你用Fiddler或Charles这类代理工具,所有HTTPS请求会直接失败,显示“request:fail net::ERR_CERT_AUTHORITY_INVALID”。这不是bug,是微信客户端内置的证书固定(Certificate Pinning)机制在起作用。解决方案不是关掉它,而是绕过它:在手机端安装代理工具的根证书,并在微信开发者工具中手动开启“不校验合法域名、https证书、TLS版本”调试开关。注意,这个开关仅对开发者工具生效,真机调试需额外处理——下文会详述。
第二,必须区分“真机”与“模拟器”的网络栈差异。开发者工具底层用的是NW.js,其网络请求走的是PC系统代理;而真机上的微信App,网络栈独立于系统设置,它有自己的代理策略。很多同学在开发者工具里抓到了包,一上真机就抓不到,原因就是没意识到:真机抓包必须通过“WiFi代理”方式,且手机和电脑必须在同一局域网。具体操作是:在电脑上启动Charles,设置Proxy → Proxy Settings → 勾选“Enable transparent HTTP proxying”,记录端口号(默认8888);在手机WiFi设置里,手动配置代理为电脑IP+该端口;最后在微信App中打开目标小程序——此时所有HTTP/HTTPS流量才会经由Charles转发。
第三,必须理解“wx.request的域名白名单”如何影响抓包结果。小程序要求所有wx.request请求的域名必须提前配置在request合法域名列表中。但这个限制只在正式版生效,开发版和体验版完全不校验。所以,抓包务必在开发版或体验版中进行。否则,你看到的404错误,可能根本不是服务端返回的,而是小程序框架在发起请求前就拦截并报错的——这种错误不会出现在抓包工具里,它压根没发出去。
2.2 真机抓包的实操陷阱与绕过方案
真机抓包最大的坑,是iOS设备对HTTPS流量的深度拦截。从iOS 10开始,系统强制要求所有HTTPS连接启用ATS(App Transport Security),而微信App作为宿主,继承了这一策略。这意味着:即使你在手机上成功安装了Charles根证书,iOS微信仍会拒绝信任该证书,导致所有HTTPS请求在Charles里显示为“Unknown”或直接失败。
我试过三种方案,只有最后一种稳定有效:
方案一:修改Info.plist添加NSAppTransportSecurity配置。无效。因为这是针对原生App的配置,微信是第三方App,你无法修改它的plist。
方案二:用mitmproxy配合iOS越狱。不现实。越狱设备无法代表真实用户环境,且绝大多数甲方明确禁止使用越狱设备进行安全测试。
方案三:利用微信开发者工具的“远程调试”能力,将真机流量重定向到本地。这是目前最可靠的方式:在开发者工具中打开“详情”→“本地设置”,勾选“启用远程调试”,记下显示的WebSocket地址(如ws://192.168.1.100:9229);然后在Chrome浏览器中访问chrome://inspect,点击Configure,添加该地址;稍等几秒,下方Devices列表就会出现“WECHAT”设备,点击“inspect”即可进入类似Chrome DevTools的调试界面。在这里,Network标签页能完整捕获所有wx.request发出的请求,包括完整的Headers、Payload、Response,且无需任何证书安装。唯一缺点是:它只能捕获通过wx.request发出的请求,无法捕获WebView内嵌页或自定义组件中通过XMLHttpRequest发出的流量——但这恰恰说明,你的小程序架构是清晰的:所有业务请求都收口在wx.request,这本身就是一种安全设计。
提示:在Chrome DevTools的Network面板中,右键任意请求 → “Save all as HAR with content”,可导出标准HAR文件。这个文件能被Burp Suite、Wireshark等专业工具直接导入分析,方便后续做自动化扫描或流量回放。
2.3 从抓包数据中识别高危模式的四条铁律
不是所有抓到的请求都值得深挖。我总结了一套快速过滤法,能在5分钟内锁定80%的高危接口:
看URL路径是否含敏感动词:
/api/v1/user/bindPhone、/pay/order/create、/admin/config/get——这类路径名直指核心业务,必须逐字段检查参数是否可伪造、响应是否含敏感信息。看请求头是否缺失关键标识:正常小程序请求,Header中必有
X-WX-KEY(微信密钥标识)、X-WX-TIMESTAMP(时间戳)、X-WX-SIGNATURE(签名)。如果某个接口完全没有这些头,或者只有Authorization: Bearer xxx这种通用Token,说明它很可能绕过了微信的签名体系,直接走传统Web认证,风险极高。看响应体是否返回原始业务数据:比如一个
/api/v1/coupon/list接口,响应里除了couponId、title,还返回了secretCode、validUntil、minOrderAmount——这些字段本该由服务端在核销时实时计算,而不是一次性全量下发。攻击者拿到secretCode就能直接调用核销接口,完全绕过前端的“仅限本人使用”逻辑。看请求参数是否过度依赖客户端生成:例如,一个提交订单的接口,参数里包含
totalPrice: 99.9、discount: 20.0、finalPrice: 79.9。这三个价格字段全由前端计算并传入,服务端只做简单相减校验。这就是典型的“金额篡改”温床。正确做法是:前端只传商品ID和数量,服务端查库计算所有价格,最终以finalPrice为准,其他字段仅作展示。
我曾在一个电商小程序里发现,其“一键复制收货地址”功能,接口响应里直接返回了receiverName、receiverPhone、receiverAddress的明文。而这个接口的调用,只需要一个简单的addressId参数。攻击者只需遍历addressId=1到addressId=1000,就能批量导出上千用户的完整隐私信息。修复方案极其简单:后端在返回前,对receiverPhone做138****1234格式化,对receiverAddress做模糊脱敏(如“北京市朝区路*号”),前端不做任何额外处理——安全水位立刻提升两个等级。
3. 反编译不是为了“看源码”,而是为了验证“你写的保护有没有被绕过”
很多开发同学觉得:“我用了webpack打包,代码都压缩混淆了,别人怎么可能看懂?”——这是最大的误解。小程序的WXML、WXSS、JS代码,在用户手机上是以纯文本形式存储的,没有任何VM字节码或加密容器。所谓“编译”,只是微信开发者工具在上传前做的资源聚合与路径映射,它不改变代码的可读性本质。反编译的目的,从来不是为了欣赏你的ES6语法有多优雅,而是为了回答一个致命问题:当攻击者拿到你发布包里的所有文件,他能否在10分钟内,精准定位到登录态校验、支付签名、敏感数据加解密的全部逻辑?
3.1 小程序包结构的本质:一个zip压缩包里的“前端工程快照”
小程序的.wxapkg文件,本质上就是一个经过特殊命名规则重打包的zip文件。它的内部结构高度标准化:
├── app-config.json # 全局配置,含pages、subNVue、permission等 ├── app-service.js # App()主入口逻辑,含onLaunch、onShow等生命周期 ├── project.config.json # 项目配置(仅开发版存在) ├── pages/ # 所有页面目录 │ ├── index/ # 首页 │ │ ├── index.wxml # 模板结构 │ │ ├── index.wxss # 样式 │ │ └── index.js # 页面逻辑 │ └── user/ # 用户页 ├── components/ # 自定义组件 ├── utils/ # 工具函数 └── project.config.json # (开发版特有)关键点在于:.wxapkg里没有node_modules,所有依赖都已被webpack或微信自己的构建工具“打平”进各个.js文件。这意味着,你看到的index.js,已经包含了lodash的debounce函数、crypto-js的AES实现、甚至你自己封装的request拦截器——它们全都在一个文件里,按执行顺序排列。反编译的第一步,就是把这个zip解压出来,然后直奔app-service.js和各pages/*/index.js。
3.2 三种主流反编译工具的实测对比与选型逻辑
市面上常见的小程序反编译工具,我全部实测过,结论很明确:不要迷信“全自动”,要相信“半自动+人工校验”。
| 工具名称 | 原理 | 优势 | 劣势 | 我的使用频率 |
|---|---|---|---|---|
| wxappUnpacker(Python脚本) | 解析.wxapkg头部魔数,按微信私有格式提取资源,再用正则还原WXML/WXSS结构 | 开源免费,命令行操作,适合批量处理多个包 | WXML还原后标签属性顺序错乱,JS代码无混淆还原,变量名仍是_0x1a2b | ★★★☆☆(仅用于快速提取资源) |
| WeChatExtension(Chrome插件) | 利用微信开发者工具的调试协议,动态注入脚本,从内存中dump运行时的WXML树和JS上下文 | 能获取真实渲染结构,含事件绑定关系,JS为未压缩原始代码 | 仅支持开发者工具,无法处理已发布线上包,需手动触发每个页面 | ★★★★★(日常调试首选) |
| wxapp-remix(Node.js工具) | 结合AST解析与字符串映射,对混淆后的JS变量名进行语义推断(如_0x1a2b[0]对应'login') | 输出接近原始源码的JS,WXML结构完整,支持导出为标准Vue/React项目结构 | 配置复杂,对强混淆(如控制流扁平化)支持弱,需手动补全AST映射表 | ★★☆☆☆(仅用于深度分析核心模块) |
我的工作流是:先用wxappUnpacker快速解压出所有文件,扫一遍app-service.js找全局配置;再用WeChatExtension在开发者工具里打开线上体验版,实时查看pages/user/index.js的运行时逻辑;最后,对怀疑存在硬编码密钥的utils/crypto.js,用wxapp-remix做深度AST还原,确认const KEY = 'abc123'是否真的写死在代码里。
注意:
WeChatExtension的使用有一个隐藏技巧。在Chrome DevTools的Console中,输入$gwx,会返回一个对象,其中$gwx.__modules__包含了所有已加载模块的原始源码(未压缩、未混淆)。你可以直接console.log($gwx.__modules__['pages/user/index.js']),把整段JS复制出来——这比任何反编译工具都准,因为它就是微信引擎实际执行的代码。
3.3 从反编译结果中定位“伪安全”设计的五个典型信号
反编译后,不要急着读代码,先做一次“信号扫描”。以下五种模式,一旦出现,基本可以判定该模块存在严重安全缺陷:
硬编码密钥(Hardcoded Secret):在JS文件中搜索
'aes-'、'des-'、'key:'、'secret:'等关键词。我见过最离谱的案例,是在utils/aes.js里写着const SECRET_KEY = 'WeChat2023!@#',而这个密钥被用来加密所有用户token。攻击者反编译拿到密钥,就能解密任意用户token,实现账号接管。明文存储敏感数据(Plain-text Storage):搜索
wx.setStorageSync、wx.setStorage,检查其value参数是否为明文。比如wx.setStorageSync('user_info', { phone: '13812345678', idCard: '110101199003072357' })——这等于把身份证号和手机号直接写进手机本地文件,任何具备root权限的App都能读取。客户端做关键校验(Client-side Only Validation):搜索
if (price < 0)、if (coupon.code.length !== 12)、if (token.expireTime > Date.now())。这些校验逻辑如果只在JS里执行,服务端不做二次校验,就是纯粹的摆设。攻击者删掉这几行JS,或用Chrome Console直接赋值price = -1,就能触发负数支付。未清理的调试代码(Debug Code Leakage):搜索
console.log、debugger、alert(、// TODO:。我曾在某政务小程序的pages/apply/index.js里,发现一段被注释掉的调试代码:// wx.request({ url: 'http://test-api.xxx.com/debug/userAll', success: res => console.log(res) })。虽然注释了,但URL和参数依然可见,攻击者只需取消注释并执行,就能获取全量用户数据。第三方SDK的默认配置(Insecure SDK Defaults):搜索
'umeng'、'bugly'、'tencent-mta'等SDK名,检查其初始化参数。很多SDK默认开启“日志上传”、“崩溃堆栈捕获”,而这些日志里可能包含用户输入的密码、银行卡号。正确做法是:在初始化时显式关闭敏感字段采集,如UMConfig.setLogEnable(false)。
有一次,我在反编译一个教育类小程序时,在app-service.js里发现这样一段代码:
App({ onLaunch: function () { const token = wx.getStorageSync('auth_token'); if (token && token !== 'expired') { // 启动时自动刷新token this.refreshToken(token); } }, refreshToken: function(token) { wx.request({ url: 'https://api.xxx.com/auth/refresh', data: { token: token }, // 注意:这里传的是本地存储的token success: res => { if (res.data.code === 0) { wx.setStorageSync('auth_token', res.data.token); // 覆盖存储 } } }); } });表面看是标准的token刷新流程。但问题在于:token是从wx.getStorageSync读取的,而这个存储本身没有任何完整性校验。攻击者只需用wx.setStorageSync('auth_token', 'fake_token')写入一个伪造token,refreshToken函数就会带着它去调用刷新接口。如果后端没有对token做签名校验,这个伪造token就可能被误认为有效,从而获得一个真实的、可长期使用的access_token。修复方案很简单:在refreshToken前,增加一步本地签名验证,或直接废弃本地token存储,改为每次从服务端获取临时凭证。
4. 抓包与反编译的交叉验证:构建“请求-逻辑-数据”的三维审计模型
单独看抓包或反编译,都只能看到安全链条的一环。真正的深度审计,必须把两者交叉起来,形成“请求怎么发 → 逻辑怎么算 → 数据怎么存”的闭环验证。我把它称为三维审计模型,这是我在给银行小程序做等保测评时,被甲方反复验证并采纳的核心方法论。
4.1 第一维:请求维度——确认“谁在发起,发给谁,带什么”
抓包得到的是静态的HTTP事务快照。我们要做的是,把每一个请求,精准映射到反编译出的JS代码行。例如,在抓包中发现一个请求:
POST /api/v1/transfer/verify HTTP/1.1 Host: api.bankxxx.com Content-Type: application/json { "fromAccount": "6228480000000000000", "toAccount": "6228480000000000001", "amount": 10000, "sign": "a1b2c3d4e5f6..." }现在,打开反编译出的pages/transfer/confirm.js,搜索/transfer/verify,找到对应的wx.request调用。重点看三处:
data对象的构造逻辑:amount是直接取this.data.inputAmount,还是经过了parseInt(this.data.inputAmount * 100)转换?前者有小数点精度风险,后者是正确的分单位处理。sign字段的生成位置:是在当前JS里调用utils/sign.js的genSign()函数,还是从某个全局变量window.SIGNER里取?前者可控,后者若SIGNER被污染则全盘失效。- 请求前的校验逻辑:是否有
if (this.data.amount <= 0) return;这样的前置判断?如果有,再看这个判断是否在wx.request之前执行——很多同学把校验写在success回调里,那就完全没意义。
4.2 第二维:逻辑维度——验证“计算过程是否可信,边界是否被覆盖”
反编译出的JS代码,是逻辑的“源代码”。但源代码不等于运行时行为。我们需要用抓包数据,反向验证逻辑的健壮性。典型场景是“金额计算”。
假设在pages/order/create.js里,有这样一段代码:
calcTotalPrice() { let total = 0; this.data.goodsList.forEach(good => { total += good.price * good.count; }); // 这里有个隐藏bug:good.price可能是字符串'99.9' return Math.round(total * 100) / 100; // 试图修复浮点误差 }抓包时,我们故意在购物车里添加一个price: '99.9'的商品,然后提交订单。观察抓包中的amount字段:如果显示为99.89999999999999,就证明Math.round没起作用,前端计算存在精度丢失。更危险的是,如果服务端也用同样逻辑校验,那么攻击者就可以构造price: '0.1'和count: 10,让前端算出1.0,但服务端算出0.9999999999999999,导致校验失败,进而暴露服务端计算逻辑的差异。
我的验证方法是:在Chrome DevTools的Console中,手动执行calcTotalPrice函数,传入各种边界数据('0.1','1e2','-1',null),观察返回值。只要有一个输入导致NaN或非预期数字,这个函数就必须重构——不是加个parseFloat就完事,而是要从数据源头(API响应)就做类型校验。
4.3 第三维:数据维度——审计“敏感信息是否落地,落地是否受控”
这是最容易被忽视,却风险最高的一维。抓包能看到传输中的数据,反编译能看到代码逻辑,但数据最终落脚在哪里?是内存、本地Storage、还是SQLite数据库?
以一个“记住密码”功能为例。抓包看到登录请求里password字段是明文;反编译看到pages/login/index.js里有wx.setStorageSync('saved_pwd', pwd)。现在,我们必须验证:这个saved_pwd,是否真的被加密存储?
方法很简单:在真机上完成一次“记住密码”操作;然后用ADB命令(Android)或iMazing工具(iOS)导出小程序的本地存储目录;搜索saved_pwd字段。如果在storage/文件夹下的某个JSON文件里,直接看到"saved_pwd":"123456",那就是赤裸裸的明文存储。正确做法是:调用wx.getFileSystemManager().writeFile,配合crypto-js的AES加密,将密码密文写入一个自定义文件,而非wx.setStorage。
我曾在一个医疗小程序里发现,其“历史报告”功能,会把完整的PDF报告Base64编码后,存入wx.setStorageSync('report_pdf', base64Str)。一份报告平均2MB,存10份就是20MB。这不仅耗尽用户手机存储,更可怕的是:任何能访问该小程序Storage的恶意软件,都能直接读取患者的所有诊断报告。修复方案是:改用wx.getFileSystemManager().writeFile,将PDF写入wx.env.USER_DATA_PATH下的临时目录,并设置keepAlive: false,让系统在小程序退出后自动清理。
4.4 三维交叉验证的实战案例:一个“分享裂变”功能的全链路审计
某电商小程序有个“邀请好友得红包”功能。用户点击分享,生成一个带inviteCode参数的链接,好友通过该链接注册,双方各得10元。我们用三维模型来审计:
请求维度(抓包):分享后,抓到一个
POST /api/v1/share/generate请求,响应里返回{ code: 0, data: { inviteCode: 'INV20231001A1B2' } }。这个inviteCode是纯随机字符串,无用户ID痕迹,初步看没问题。逻辑维度(反编译):在
pages/share/index.js里,找到generateInviteCode函数:generateInviteCode() { const timestamp = Date.now().toString(36); // 转36进制 const rand = Math.random().toString(36).substr(2, 4); return 'INV' + timestamp + rand; }问题来了:
Date.now()是客户端时间,可被随意篡改。攻击者把手机时间调到2099年,生成的inviteCode就全是INVz...开头,而服务端如果只做简单前缀校验(code.startsWith('INV')),就可能放过这个异常码。数据维度(存储):反编译发现,生成的
inviteCode被存入wx.setStorageSync('my_invite_code', code)。抓包时又发现,分享链接里?inviteCode=INV20231001A1B2是直接拼接的,没有任何签名。这意味着:攻击者可以伪造任意inviteCode,只要格式符合,就能触发奖励发放。
交叉结论:这个功能存在双重风险。一是inviteCode生成逻辑可预测,二是分享链接无签名,导致奖励逻辑完全失控。修复必须同步进行:服务端生成inviteCode并签名,前端只负责展示和传播;本地存储改为加密存储;所有涉及奖励的接口,必须校验inviteCode签名及有效期。
5. 安全加固的七条落地建议:不写PPT,只给能立刻执行的代码行
所有安全方案,最终都要落到代码上。以下七条,是我从27个金融小程序中提炼出的、经甲方生产环境验证的加固措施。每一条,都附带可直接复制粘贴的代码片段,以及为什么这么写的底层逻辑。
5.1 对所有wx.request请求,强制添加微信签名头
微信官方文档提到wx.request支持header参数,但没强调必须用它做签名。很多团队把签名逻辑写在业务层,导致部分接口遗漏。最佳实践是:全局拦截wx.request,统一注入签名。
// utils/request.js const originalRequest = wx.request; wx.request = function(options) { // 1. 获取当前时间戳(毫秒) const timestamp = Date.now(); // 2. 生成随机字符串(16位) const nonceStr = Math.random().toString(36).substr(2, 16); // 3. 构造待签名字符串(按字典序拼接) const signStr = `timestamp=${timestamp}&nonceStr=${nonceStr}&url=${options.url}`; // 4. 使用小程序AppSecret计算SHA256签名(AppSecret绝不写在前端!此处仅为示意,实际应由服务端下发) // 正确做法:在wx.login成功后,用code向服务端换取一个短期有效的signKey,缓存在内存中 const signKey = getApp().globalData.signKey || ''; const signature = CryptoJS.HmacSHA256(signStr, signKey).toString(); // 5. 注入请求头 options.header = { ...options.header, 'X-WX-TIMESTAMP': timestamp, 'X-WX-NONCE-STR': nonceStr, 'X-WX-SIGNATURE': signature }; // 6. 调用原始request return originalRequest(options); };为什么有效:服务端收到请求后,用同样的算法重新计算签名,比对X-WX-SIGNATURE。只要AppSecret不泄露,攻击者就无法伪造合法签名。即使他抓到了一次请求,nonceStr和timestamp也已失效。
5.2 敏感数据存储,必须用文件系统加密,禁用wx.setStorage
wx.setStorage的数据,位于微信App的沙盒目录,但并非加密存储。iOS上可通过iTunes备份导出,Android上root后可直接读取。必须迁移到wx.getFileSystemManager()。
// utils/secureStorage.js const fs = wx.getFileSystemManager(); const KEY = 'your-aes-key-32-bytes-long'; // 实际应从服务端动态获取 function encrypt(data) { const iv = CryptoJS.enc.Utf8.parse('1234567812345678'); // 初始化向量,固定即可 const key = CryptoJS.enc.Utf8.parse(KEY); const encrypted = CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } function saveSecure(key, value) { const encrypted = encrypt(JSON.stringify(value)); const filePath = `${wx.env.USER_DATA_PATH}/${key}.enc`; fs.writeFile({ filePath: filePath, data: encrypted, encoding: 'utf8', success: () => console.log('Secure save success'), fail: err => console.error('Secure save fail', err) }); } // 使用示例 saveSecure('user_token', { accessToken: 'xxx', expireAt: 1700000000 });为什么有效:USER_DATA_PATH是微信为每个小程序分配的独立目录,且writeFile写入的文件,系统层面做了基础隔离。配合AES加密,即使设备被物理获取,也无法直接读取明文。
5.3 所有金额、数量类字段,必须在服务端二次校验,前端仅作展示
前端计算永远不可信。wx.request的data里,只允许传原始业务参数(如goodsId,count),所有衍生计算(totalPrice,discount)必须由服务端完成。
// pages/order/create.js submitOrder() { // ❌ 错误:前端计算总价 // const totalPrice = this.data.goodsList.reduce((sum, g) => sum + g.price * g.count, 0); // ✅ 正确:只传原始参数 const orderData = { goodsList: this.data.goodsList.map(g => ({ goodsId: g.id, count: g.count })) }; wx.request({ url: 'https://api.xxx.com/order/create', method: 'POST', data: orderData, success: res => { // 服务端返回的finalPrice,才是唯一可信值 this.setData({ finalPrice: res.data.finalPrice }); } }); }为什么有效:服务端查库获取最新价格,实时计算满减、优惠券,避免前端因缓存、时钟漂移、JS精度等问题导致的计算偏差。攻击者篡改count,只会让服务端返回“库存不足”错误,无法绕过业务规则。
5.4 页面跳转时,禁止携带敏感参数,改用全局状态管理
很多同学习惯在wx.navigateTo的url里拼接?userId=123&token=xxx,这是重大风险。URL参数会被系统日志、第三方统计SDK捕获。
// ❌ 危险:URL传参 wx.navigateTo({ url: '/pages/user/profile?userId=123&token=abc' }); // ✅ 安全:用getApp().globalData传递 const app = getApp(); app.globalData.targetUserId = 123; app.globalData.targetToken = 'abc'; wx.navigateTo({ url: '/pages/user/profile' }); // 在profile.js的onLoad里获取 onLoad() { const app = getApp(); this.setData({ userId: app.globalData.targetUserId, token: app.globalData.targetToken }); // 使用后立即清空,防止内存泄漏 app.globalData.targetUserId = null; app.globalData.targetToken = null; }为什么有效:globalData是内存变量,生命周期与小程序进程一致,不会被外部进程读取。且onLoad后立即清空,确保敏感数据不长期驻留。
5.5 关键业务操作,必须加入二次确认弹窗,并绑定生物识别
对于支付、删除、授权等高危操作,不能只靠wx.showModal。必须调用wx.checkIsSupportSoterAuthentication,启用指纹/面容ID。
async confirmWithBiometric(title, content) { try { // 1. 检查设备是否支持 const support = await wx.checkIsSupportSoterAuthentication(); if (!support.supportSoter) { return wx.showModal({ title, content }); } // 2. 发起生物识别 const auth = await wx.startSoterAuthentication({ requestAuthModes: ['fingerPrint', 'facial'], challenge: 'auth_' + Date.now(), authContent: '请验证身份以继续操作' }); if (auth.errCode === 0) { return { confirm: true }; } else { return { confirm: false, errMsg: '生物识别失败' }; } } catch (e) { return wx.showModal({ title, content: e.message || content }); } } // 使用 const result = await this.confirmWithBiometric( '确认删除?', '此操作不可撤销,将永久删除该订单' ); if (result.confirm) { // 执行删除 }为什么有效:生物识别是硬件级安全通道,比任何软件弹窗都可靠。微信的startSoterAuthentication会调用系统API,攻击者无法通过模拟点击绕过。
5.6 所有第三方SDK,必须在初始化时关闭敏感数据采集
以友盟统计为例,默认会上报设备ID、地理位置、页面停留时长。这些都可能关联到用户身份。
// app.js onLaunch() { // 初始化友盟,显式关闭所有敏感选项 umsdk.init({ appKey: 'your-app-key', reportCrash: false, // 关闭崩溃上报 autoTrack: { appLaunch: false, // 不自动上报启动 pageView: false, // 不自动上报页面浏览 click: false // 不自动上报点击 }, // 手动上报需要的数据 customTrack: { event: 'user_login_success', attributes: { userType: 'vip' } } }); }为什么有效:最小权限原则。只收集业务必需的数据,避免因SDK漏洞导致用户隐私大规模泄露。
5.7 构建自动化检测脚本,每日扫描新上线包
安全不能靠人工。我用Node.js写了一个脚本,每天凌晨自动下载最新体验版.wxapkg,执行三项检查:
- 搜索所有JS文件,匹配硬编码密钥正则 `/['"](\w{4,}key|secret
