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连接启用ATSApp 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参数。攻击者只需遍历addressId1到addressId1000就能批量导出上千用户的完整隐私信息。修复方案极其简单后端在返回前对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 三种主流反编译工具的实测对比与选型逻辑市面上常见的小程序反编译工具我全部实测过结论很明确不要迷信“全自动”要相信“半自动人工校验”。工具名称原理优势劣势我的使用频率wxappUnpackerPython脚本解析.wxapkg头部魔数按微信私有格式提取资源再用正则还原WXML/WXSS结构开源免费命令行操作适合批量处理多个包WXML还原后标签属性顺序错乱JS代码无混淆还原变量名仍是_0x1a2b★★★☆☆仅用于快速提取资源WeChatExtensionChrome插件利用微信开发者工具的调试协议动态注入脚本从内存中dump运行时的WXML树和JS上下文能获取真实渲染结构含事件绑定关系JS为未压缩原始代码仅支持开发者工具无法处理已发布线上包需手动触发每个页面★★★★★日常调试首选wxapp-remixNode.js工具结合AST解析与字符串映射对混淆后的JS变量名进行语义推断如_0x1a2b[0]对应login输出接近原始源码的JSWXML结构完整支持导出为标准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)写入一个伪造tokenrefreshToken函数就会带着它去调用刷新接口。如果后端没有对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; }); // 这里有个隐藏buggood.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)。抓包时又发现分享链接里?inviteCodeINV20231001A1B2是直接拼接的没有任何签名。这意味着攻击者可以伪造任意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.setStoragewx.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里拼接?userId123tokenxxx这是重大风险。URL参数会被系统日志、第三方统计SDK捕获。// ❌ 危险URL传参 wx.navigateTo({ url: /pages/user/profile?userId123tokenabc }); // ✅ 安全用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
微信小程序安全实战:抓包与反编译交叉审计指南
发布时间:2026/5/23 11:15:09
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连接启用ATSApp 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参数。攻击者只需遍历addressId1到addressId1000就能批量导出上千用户的完整隐私信息。修复方案极其简单后端在返回前对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 三种主流反编译工具的实测对比与选型逻辑市面上常见的小程序反编译工具我全部实测过结论很明确不要迷信“全自动”要相信“半自动人工校验”。工具名称原理优势劣势我的使用频率wxappUnpackerPython脚本解析.wxapkg头部魔数按微信私有格式提取资源再用正则还原WXML/WXSS结构开源免费命令行操作适合批量处理多个包WXML还原后标签属性顺序错乱JS代码无混淆还原变量名仍是_0x1a2b★★★☆☆仅用于快速提取资源WeChatExtensionChrome插件利用微信开发者工具的调试协议动态注入脚本从内存中dump运行时的WXML树和JS上下文能获取真实渲染结构含事件绑定关系JS为未压缩原始代码仅支持开发者工具无法处理已发布线上包需手动触发每个页面★★★★★日常调试首选wxapp-remixNode.js工具结合AST解析与字符串映射对混淆后的JS变量名进行语义推断如_0x1a2b[0]对应login输出接近原始源码的JSWXML结构完整支持导出为标准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)写入一个伪造tokenrefreshToken函数就会带着它去调用刷新接口。如果后端没有对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; }); // 这里有个隐藏buggood.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)。抓包时又发现分享链接里?inviteCodeINV20231001A1B2是直接拼接的没有任何签名。这意味着攻击者可以伪造任意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.setStoragewx.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里拼接?userId123tokenxxx这是重大风险。URL参数会被系统日志、第三方统计SDK捕获。// ❌ 危险URL传参 wx.navigateTo({ url: /pages/user/profile?userId123tokenabc }); // ✅ 安全用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