行业资讯

小程序路由跳转权限漏洞实战:从IDOR漏洞挖掘到纵深防御方案

发布时间:2026/7/22 4:52:47
小程序路由跳转权限漏洞实战:从IDOR漏洞挖掘到纵深防御方案 1. 项目概述当小程序遇上Web测一场关于权限的攻防演练最近在复盘一些移动端安全测试项目时我反复琢磨一个场景很多团队在开发小程序时会不自觉地沿用Web开发的思维尤其是在页面路由跳转这块。乍一看wx.navigateTo、wx.redirectTo这些API和Web的window.location.href或Vue Router的push方法很像都是从一个页面跳到另一个页面。但正是这种“像”埋下了不少安全隐患。我手头就有一个挺典型的实战案例一个宠物社交类的小程序其核心的“个人中心”到“订单详情”的跳转逻辑就存在一个隐蔽的权限校验漏洞攻击者可以绕过前端鉴权直接访问他人的敏感订单数据。这个漏洞的挖掘过程本质上就是把小程序当成一个特殊的“Web应用”来测重点审视其路由跳转机制是否被“想当然”地安全实现了。今天我就把这个案例从头到尾拆解一遍聊聊如何系统性地挖掘这类漏洞以及开发侧该如何加固。2. 核心思路拆解为什么路由跳转会成为权限的“后门”在深入案例之前我们得先搞清楚小程序的路由跳转和传统Web应用到底有什么根本区别以及这些区别如何催生漏洞。2.1 小程序与Web路由的核心差异很多人觉得小程序是“封闭”的不如Web开放所以更安全。这个观点对了一半。小程序的沙箱环境和API白名单机制确实阻止了很多常规Web攻击比如任意JS执行、DOM操作。但在页面间通信和数据流转这个层面上小程序提供的方式反而更“原始”或说更“受限”迫使开发者采用一些有风险的模式。在Web单页应用SPA里比如用Vue或React开发路由状态通常由前端的路由库如Vue Router集中管理。权限校验可以做成全局路由守卫。每次跳转前守卫都会介入检查用户的登录状态、角色权限决定是否放行。这个检查是强制性的、中心化的。// Vue Router 全局前置守卫示例 router.beforeEach((to, from, next) { if (to.meta.requiresAuth !store.state.isLoggedIn) { next(/login); // 未登录则跳转到登录页 } else { next(); // 放行 } });而小程序呢它没有“全局路由守卫”这个概念。页面跳转主要依靠微信提供的几个APIwx.navigateTo保留当前页面跳转新页面、wx.redirectTo关闭当前页面跳转新页面、wx.switchTab跳转到tabBar页面等。这些API调用后微信客户端会负责渲染新页面。权限校验的责任完全落在了每个页面的独立逻辑上。通常开发者会在目标页面的onLoad生命周期函数里检查传入的参数和本地存储的登录状态。// 小程序页面 pageA.js - 发起跳转 Page({ goToOrderDetail() { wx.navigateTo({ url: /pages/order/detail?id12345 // 传递订单ID }); } }) // 小程序页面 order/detail.js - 目标页面 Page({ onLoad(options) { const orderId options.id; // 获取URL参数中的订单ID // 开发者需要在这里手动检查当前登录用户是否有权查看orderId12345的订单 this.checkOrderPermission(orderId); }, checkOrderPermission(orderId) { // 这里需要调用后端接口验证 orderId 是否属于当前用户 // 如果忘记调用或逻辑有误漏洞就产生了 } })看到问题了吗在Web SPA中守卫是“门神”每个想进门的人都要被查。在小程序中每个房间页面自己负责看门而且这个“看门”动作checkOrderPermission是在人已经进了房间onLoad执行之后才发生的。如果某个房间忘记安排看门人或者看门人只是草草看了一眼邀请函URL参数就放行那么非法闯入就发生了。2.2 漏洞产生的典型模式基于上述差异我总结了两种最常见的导致权限漏洞的模式完全缺失服务端校验目标页面的onLoad函数仅根据传入的ID去后端请求数据后端接口没有对“当前登录用户”和“请求的资源ID”做归属关系校验。这是最严重、最低级的漏洞。脆弱的客户端校验目标页面尝试做校验但逻辑有缺陷。例如依赖不可信的前端参数比如除了订单ID还传了一个userId参数然后在前端比较这个userId和本地存储的userId是否一致。攻击者可以轻易修改URL中的userId。校验逻辑可被绕过例如校验函数checkOrderPermission因为代码逻辑错误如错误使用||和、异步处理问题校验未完成就渲染数据而导致失效。历史页面栈信息泄露小程序getCurrentPages()能获取页面栈有些开发者会把敏感信息如用户ID存在页面对象的data里攻击者可能通过某些方法如利用开发者工具或内存调试读取到这些信息进而构造攻击。我们今天的案例就是第一种和第二种模式的结合体。3. 实战案例深度拆解宠物社交小程序的订单越权访问下面进入正题来看这个我实际测试过的宠物社交小程序。3.1 目标功能与正常流程这个小程序有个“我的订单”页面/pages/order/list列出当前用户的所有订单。点击任一订单会跳转到订单详情页/pages/order/detail。正常的前端代码逻辑如下// pages/order/list.js - 订单列表页 Page({ data: { orders: [] }, onLoad() { // 假设从后端获取当前用户的订单列表 this.fetchMyOrders(); }, onTapOrder(e) { const orderId e.currentTarget.dataset.id; // 正常跳转传递订单ID wx.navigateTo({ url: /pages/order/detail?id${orderId} }); } }) // pages/order/detail.js - 订单详情页 Page({ data: { orderDetail: null }, onLoad(options) { const orderId options.id; console.log(接收到的订单ID:, orderId); // 这里直接调用获取详情的接口缺少了权限校验步骤。 this.fetchOrderDetail(orderId); }, fetchOrderDetail(orderId) { // 调用后端接口 /api/order/detail?orderIdxxx wx.request({ url: https://api.example.com/order/detail, data: { orderId: orderId }, success: (res) { // 假设后端也没有做校验直接返回了订单详情 this.setData({ orderDetail: res.data }); } }); } })正常的后端接口存在漏洞的版本# 伪代码假设是Python Flask后端 app.route(/api/order/detail) def get_order_detail(): order_id request.args.get(orderId) # 漏洞点仅根据order_id查询未关联当前登录用户 order Order.query.filter_by(idorder_id).first() return jsonify(order.to_dict()) if order else jsonify({})3.2 漏洞挖掘与利用过程我的测试步骤如下抓包与接口分析首先使用抓包工具如Charles或Fiddler代理手机流量操作小程序查看自己用户A的订单详情。抓取到网络请求为GET https://api.example.com/order/detail?orderId1001。参数篡改测试在抓包工具中我将请求中的orderId参数值从1001我的订单修改为1002猜测的其他订单ID然后重放Replay这个请求。结果观察后端直接返回了订单ID为1002的完整详情包括收货地址、电话、商品信息等。这说明后端接口/api/order/detail存在未授权访问漏洞IDORInsecure Direct Object Reference。前端路由跳转测试漏洞不仅存在于直接调用接口。我尝试在小程序前端通过修改URL直接跳转。由于小程序不能像浏览器那样直接输入地址栏我采用了两种方法方法一利用Webview或漏洞。如果小程序内有任何可以加载外部H5的Webview组件并且能控制其URL可能可以构造一个页面来执行wx.navigateTo。但本例不涉及。方法二模拟页面栈操作需特定条件。更通用的方法是如果开发者不小心在app.js的onLaunch或某个全局函数里根据场景值scene或其他参数直接进行了页面跳转且参数可控也可能成为入口。不过对于这个订单详情页最直接的“利用”就是第一步的接口重放。前端路由的漏洞本质是允许用户构造一个指向任意订单详情的跳转链接而小程序API本身无法阻止这种构造。构造攻击链作为一个攻击者我如何系统性地获取他人订单ID我可以先注册一个账号产生少量订单如ID为1001, 1002。然后我就可以通过遍历ID如1000-1100的方式批量重放请求窃取大量其他用户的订单数据。由于订单ID通常是自增整数这种攻击成本极低。注意在实际测试中务必在获得授权的范围内进行。切勿对未授权的系统进行任何形式的攻击测试。3.3 漏洞原理与危害总结这个案例的漏洞原理非常清晰前端在跳转到详情页时仅传递了资源IDorderId并默认后端会做好校验。后端接收资源ID后没有检查该资源是否属于当前发起请求的认证用户通常通过wx.login获得的openid或自定义token来识别直接返回数据。其危害是直接的敏感数据泄露。利用此漏洞攻击者可以查看任意用户的订单信息获取手机号、地址等个人隐私甚至可能衍生出诈骗、恶意下单等二次风险。4. 系统性挖掘方法论不止于订单ID挖到一个IDOR漏洞很有成就感但作为专业的安全测试我们需要一套系统的方法来检查小程序中所有可能的路由跳转点。以下是基于我经验的检查清单4.1 定位所有路由跳转点全局搜索关键API在小程序源码如果通过反编译获得或通过静态分析工具搜索wx.navigateTo、wx.redirectTo、wx.reLaunch、wx.switchTab等所有跳转API的调用位置。分析跳转参数对每个跳转点记录其url参数。重点关注其中是否拼接了变量。例如/pages/user/profile?id${userId}(高风险)/pages/article/detail?postId${postId}(高风险)/pages/shop/index?merchantId${mchId}(高风险)/pages/index(无参数低风险)梳理参数来源确定这些变量userId,postId从哪里来。是来自上一个页面的data还是来自onLoad的options或是来自全局app.globalData追踪其数据流判断用户是否有可能控制这些变量的值。4.2 测试每个跳转点的权限控制对于每个携带参数尤其是ID类参数的跳转点进行如下测试正常流程测试使用测试账号A走一遍正常流程用抓包工具记录下跳转时生成的完整URL和发出的所有网络请求。参数篡改测试修改URL参数如果小程序有Webview或某些特殊配置允许从外部链接跳转如navigator组件的open-type为navigate且url来自后端尝试直接修改链接中的ID值。修改请求参数对于更常见的情况重点放在抓包修改上。拦截跳转后发出的第一个关键数据请求通常是目标页面的onLoad中发出的初始化请求修改其中的ID参数为其他用户的同类ID观察响应。平行越权测试准备两个同权限等级的测试账号A和B。用A账号操作获取A的资源ID如A的订单ID1001。然后在B账号的登录状态下尝试访问/pages/order/detail?id1001通过抓包重放或模拟请求。系统应返回“无权访问”或空数据而不是B的订单详情或A的订单详情。垂直越权测试准备不同权限等级的账号如普通用户C和管理员D。尝试在C的权限下访问本应只有D才能访问的页面或数据如/pages/admin/userList。即使前端隐藏了入口也可能通过直接猜测路径和参数进行访问。4.3 辅助工具与技巧抓包工具配置抓取小程序流量需要安装CA证书到手机并配置代理。对于安卓相对容易对于iOS需要描述文件。注意微信7.0版本对证书校验更严格可能需要使用旧版微信或特定版本的抓包工具如Charles 4.6.1的“SSL Proxying”设置。反编译小程序对于更深入的黑盒或灰盒测试可以尝试反编译小程序包.wxapkg。这能让你直接看到前端的路由逻辑、API请求地址和部分硬编码的逻辑。但这涉及法律和授权问题必须在获得明确授权的前提下进行。接口模糊测试在发现像/api/order/detail这样的接口模式后可以使用工具如Burp Suite的Intruder对orderId参数进行批量遍历快速发现数据泄露问题。5. 修复方案从前端到后端的纵深防御发现了漏洞更要知道如何修复。这里给出从浅到深的加固方案。5.1 后端修复治本之策这是最关键、最必须的一步。所有涉及用户数据的接口必须在服务端进行严格的权限校验。修复后的后端接口示例# Python Flask 修复版 app.route(/api/order/detail) def get_order_detail(): order_id request.args.get(orderId) # 1. 从请求头或Cookie中获取当前用户的身份标识如session_key或自定义token current_user_id get_current_user_id_from_token(request.headers.get(Authorization)) if not current_user_id: return jsonify({code: 401, msg: 未授权}), 401 # 2. 查询时将订单ID与用户ID关联 order Order.query.filter_by(idorder_id, user_idcurrent_user_id).first() if not order: # 即使订单存在但不属于当前用户也返回“无权限”或“未找到”避免信息泄露 return jsonify({code: 403, msg: 无权访问该订单}), 403 return jsonify({code: 200, data: order.to_dict()})核心原则服务端永远不要相信客户端传来的资源ID归属。每次数据查询都必须将资源ID与当前认证用户的身份进行绑定校验。5.2 前端加固辅助防御虽然前端无法从根本上阻止恶意请求但良好的实践能增加攻击门槛并遵循“防御性编程”原则。页面跳转前的预校验在发起wx.navigateTo之前如果可能先进行一次轻量级的权限确认。例如在订单列表页点击某个订单时这个订单ID本就来自后端返回的、属于当前用户的列表。这至少保证了跳转源头的ID是合法的。但这不能防止攻击者直接构造请求。// pages/order/list.js - 改进版 onTapOrder(e) { const orderId e.currentTarget.dataset.id; const myOrderIds this.data.orders.map(o o.id); // 来自后端的安全列表 // 简单检查要跳转的ID是否在自己的订单列表中 if (!myOrderIds.includes(orderId)) { wx.showToast({ title: 非法操作, icon: none }); return; } wx.navigateTo({ url: /pages/order/detail?id${orderId} }); }目标页面的防御性代码在详情页的onLoad中即使认为后端会校验也应加入基本的防御和友好的错误提示。// pages/order/detail.js - 改进版 onLoad(options) { const orderId options.id; // 基础校验ID是否存在且格式正确 if (!orderId || isNaN(parseInt(orderId))) { wx.showModal({ title: 错误, content: 订单参数无效, showCancel: false, success: () { wx.navigateBack(); } }); return; } this.fetchOrderDetail(orderId); }, fetchOrderDetail(orderId) { wx.showLoading({ title: 加载中 }); wx.request({ url: https://api.example.com/order/detail, data: { orderId: orderId }, success: (res) { if (res.data.code 200) { this.setData({ orderDetail: res.data.data }); } else if (res.data.code 403) { // 明确处理无权限情况 wx.showModal({ title: 无权限, content: 您无权查看此订单, showCancel: false, success: () { wx.navigateBack(); } }); } else { wx.showToast({ title: 加载失败, icon: error }); } }, fail: (err) { /* 处理网络错误 */ }, complete: () { wx.hideLoading(); } }); }避免在页面对象中存储敏感信息不要在Page的data或全局变量中存储其他用户的ID、Token等。使用后及时清理。5.3 架构层面建议使用不可预测的标识符避免使用自增整数1,2,3...作为资源ID。可以使用UUID、雪花算法生成的ID或者将自增ID进行加密混淆后再传给前端。这样能有效增加攻击者猜测其他有效ID的难度。引入中间层或路由守卫抽象虽然小程序没有官方路由守卫但可以自己封装一个跳转工具函数在其中统一加入一些逻辑比如检查登录状态、记录跳转日志等。// utils/navigator.js const navigateToWithAuth (url) { const app getApp(); if (!app.globalData.hasLogin) { wx.redirectTo({ url: /pages/login/login }); return; } // 这里可以加入其他统一逻辑如参数检查、埋点等 wx.navigateTo({ url }); }; // 在页面中调用 navigateToWithAuth(/pages/order/detail?id${orderId});关键操作增加二次确认或风控对于查看非常敏感的页面如支付结果、身份证信息可以要求输入密码、验证码或进行人脸识别等二次验证。6. 测试经验与避坑指南在长期的小程序安全测试中我积累了一些宝贵的经验和容易踩的坑不要只测正向流程绝大多数开发者和测试人员都只测试“正确”的路径。安全测试的核心是思考“如果用户不按常理出牌会怎样”。务必测试修改参数、跳过步骤、直接访问URL等异常行为。关注“隐藏”的传参方式除了wx.navigateTo的url参数小程序页面间通信还有EventChannel事件通道和全局数据getApp().globalData。这些方式也可能传递敏感ID需要一并检查。注意缓存数据小程序有本地存储wx.setStorageSync。如果某个页面把用户ID或Token明文存在本地然后另一个页面读取并使用攻击者可能通过篡改本地存储来实施攻击。确保敏感信息存储安全如加密或尽量使用服务端Session。组合漏洞往往更致命一个单纯的路由跳转IDOR可能危害有限。但如果结合了其他漏洞比如信息泄露漏洞某个接口返回了所有用户的订单ID列表。批量请求漏洞后端没有对/api/order/detail这样的接口做频率限制。越权访问漏洞就是我们今天讨论的。 三者结合攻击者就可以先通过接口1拿到所有ID再通过接口2快速批量窃取所有订单详情造成大规模数据泄露。沟通与报告发现漏洞后撰写清晰的安全报告至关重要。报告中应包括漏洞类型如IDOR、受影响的功能点、复现步骤Step-by-Step、请求/响应截图、潜在危害等级可参考CVSS评分以及具体的修复建议就像本章节给出的那样。清晰的报告能帮助开发团队快速理解和解决问题。小程序的安全是一个需要前端、后端、架构师共同关注的系统工程。把小程序当成Web来测重点就是要打破“客户端可控环境”的幻觉始终坚持“服务端校验为黄金准则”的原则。每一次路由跳转每一次参数传递都要在脑子里多问一句“如果这个参数被篡改了我的系统还安全吗” 多问这一句可能就堵住了一个潜在的数据泄露漏洞。