多步流程中的越权漏洞:原理、攻击手法与修复实践
别把越权漏洞想得太玄它其实就是一类“拿着别人的钥匙开自己门”的问题。权限滥用这个说法在安全圈里见得太多但真正有意思的往往不是单点接口的漏校验而是藏在多步流程里的那种越权——步骤一多每个环节都以为“上一步已经校验过了”结果整个流程变成了一扇四处漏风的门。这篇文章就围绕权限滥用和多步流程中的越权漏洞展开说说这类问题怎么产生、怎么测、怎么修适合正在做业务安全测试、搞代码审计或者纯粹想搞懂Web权限机制的读者。1. 越权漏洞的本质多步流程里的“信任接力”1.1 先分清两类越权水平越权和垂直越权越权漏洞大体分两种。水平越权通俗讲就是同级用户之间互相串门——你登录了自己的账号却能看到或者操作别人的订单、别人的文档、别人的申请单。垂直越权则是低级别用户去执行高级别操作——普通员工调用了一个只有管理员能用的接口把自己角色改成了管理员或者直接审批了本该由总监审批的流程。很多团队会把这两类问题混在一起定性但排查和修复思路完全不同。水平越权核心在“对象归属校验”垂直越权核心在“操作权限校验”。这两种问题一旦混进多步流程事情就开始变得棘手。1.2 为什么步骤一多权限就兜不住单接口场景下开发通常会在函数入口判断“当前登录用户是否拥有该资源”出了问题也容易定位。但在多步流程里情况变成了这样第一步接口校验了用户A的身份返回了一个临时标识第二步接口拿到这个标识没再校验身份直接用标识去查数据第三步接口甚至只信任前端传来的参数把流程状态也交给了客户端。每一步都默认“前一步帮我验过了”形成一条信任接力链。链条里任何一个环节没接住整个流程就会沦陷。最典型的例子就是找回密码流程输入账号、接收验证码、设置新密码三步接口如果权限校验不完整攻击者完全可以通过篡改账号参数把别人的密码改掉。还有一个常被忽视的点是状态归属不清晰。多步流程通常带一个“流程实例”概念比如申请单、工单、审批流。问题在于这个流程实例属于谁、当前该由谁操作后端往往没有明确建模。于是谁拿到流程ID谁就能操作这个流程——这就是权限滥用的温床。2. 高发场景拆解到底哪些流程容易出事2.1 密码找回与账号安全流程这类流程每一步都涉及敏感操作攻击面也最直接。常见漏洞模式是第一步用邮箱或手机号发验证码第二步校验验证码第三步设置新密码。问题往往出在“校验验证码通过后服务端如何在后续步骤中记住这个身份”。如果服务端只是把账号名放在一个临时Session里第三步接口再根据前端提交的账号名来改密码那就有文章可做了。攻击者可以先用自己的账号走完前两步到第三步时把账号名改成受害者的账号直接完成撞改。更隐蔽的做法是第二步接口响应里返回了一个token或者确认串第三步接口信任这个串但token没有和用户、会话、有效期正确绑定攻击者拿到别人的确认串照样能用。2.2 审批流与工单系统后台系统里的审批流是垂直越权的高发地。一个典型的报销审批流程是员工提交申请、直属leader审批、财务复核、出纳打款。这套流程看上去层层把关但接口设计可能只有一个“审核”接口参数里带上申请单ID和动作同意/驳回后端只检查了“这个单子存在吗”没检查“当前登录人是否处于该单子的待审批人位置”。结果就是普通员工只要知道申请单ID自己审核自己的单子或者某个环节的操作人已经更换但旧的审批人权限没有失效仍然能对流程进行干预。这种问题在权限频繁变动的企业组织架构里尤其容易出现因为角色和流程绑定关系只在创建单子时固定后续变更没有同步更新。2.3 电商订单与售后流程订单生命周期长、状态多天然就是越权漏洞的富矿。常见多步流程包括下单、改地址、申请退款、退货、补差价。每一步都可能存在水平越权隐患。我之前测试过一套电商系统它的“修改订单地址”功能分两步第一步校验订单归属返回修改页第二步提交新地址时后端直接用前端传来的orderId去执行更新没有做第二次归属校验。攻击者完全可以在第一步用自己订单走通流程然后在第二步把orderId替换成受害者的订单ID——地址直接被改掉后续快递就会发到错误的地方再配合“忘记密码”流程几乎等于接管了整个账号。2.4 多步骤表单与草稿保存问卷调查、考试答题、长表单录入这类场景通常会把一个表单拆成多步前端分页展示每一步点击“保存草稿”。这类系统有个通病草稿用draftId来唯一标识后端提供的保存接口只校验draftId是否存在不校验当前登录用户是否创建了该草稿。这意味着攻击者只要遍历draftId就能读取别人的草稿内容也能往别人的草稿里写入恶意数据。放到考试系统里直接变成“帮全班同学提交答案”的惨案。这种漏洞往往测试时容易忽略因为它不像订单那样有明确的归属关联很多开发者在写保存接口时根本没有把用户绑定到草稿上这个意识。2.5 服务端中间回调与异步通知多步流程里还有一类“隐形的步骤”——服务端回调。比如支付平台同步通知、第三方审核结果回传这类流程往往不做鉴权或者只校验一个固定header因为开发者认为回调来自可信源。一旦这个回调地址或者参数被猜出来攻击者可以直接模拟平台给业务系统发送请求把订单状态改成已支付或者把审核状态改成通过。这类问题虽然不只是越权但和多步流程的权限滥用关系密切排查时需要一并关注。3. 攻击手法拆解五类典型的利用方式3.1 中间态直接访问很多多步流程接口只暴露给了前端但不代表不可直接调用。攻击者的常规操作是正常走第一步拿到一个中间态标识然后尝试直接请求第二步、第三步、甚至最后一步的接口。跳步攻击的目标是判断“后续接口是否校验了前置步骤的合法性”。比如一个多步骤身份认证流程前端是第1步填身份信息、第2步活体检测、第3步完成。如果第3步接口只校验了前端传来的“步骤完成标记”攻击者直接POST到第3步接口并携带完全任意的标记就能跳过活体检测完成全套流程。这种问题本质上是服务端没有维护每个用户当前的流程状态机。3.2 ID枚举与替换前面提到的订单、草稿、申请单如果主键是自增整数攻击者只需要遍历一个范围就能拿到全量流程ID。实际操作时攻击者会先用自己的账号创建一条流程记录下系统分配的ID然后对ID做加减运算逐个请求步骤接口观察响应差异。比如创建一条申请单拿到ID10086访问ID10087看看是不是别人的单子。如果是就用这个ID调用审核接口、作废接口、修改接口。ID是否可枚举只是加固层面的事真正的防护重心始终应该是“每个接口都必须校验当前操作人与流程对象的关系”。3.3 前端参数完全可信有些流程把业务数据全部放在前端后端只是存储。攻击者打开浏览器开发者工具改一下隐藏域里的userId、role、step、status这些参数再提交一遍后端照单全收。这类问题的根源是“后端没有对提交的字段做二次校验”。举例来说注册流程的第三步“完善资料”后端根据前端传来的role字段决定用户是普通用户还是管理员——这种设计一旦出现垂直越权就一键击穿。我在代码审计时经常会全局搜索request中的参数是否被直接用于权限判断或角色判断只要搜到类似String role request.getParameter(role)这样的语句基本可以判定存在漏洞。3.4 流程对象混淆与串台多步流程里经常出现“共享流程对象”的场景同一批工单分配给一组客服处理每个客服登录后都能看到队列中的工单。如果权限模型设计成“队列里的所有工单当前操作人都能访问”那当一个攻击者混进这个队列时就能操作所有工单。还有一种串台场景是流程模板全局共享。比如请假流程里普通员工提交申请后自动流转到“直属上级”节点。实现时如果只是根据提交人的部门找到某一个负责人ID没有把“该负责人是否就是当前审批人”绑定到流程实例上就可能出现同一个流程流转到了所有同级别的审批人手里人人都能操作。3.5 竞态条件下的权限绕过竞态条件在越权场景里也很常见尤其在设计不良的多步流程里。攻击者的思路是同时发送多个并发请求利用服务端逻辑的间隙绕过权限校验。典型场景一个订单分两步提交第一步预扣库存返回订单号第二步确认订单。攻击者同时发送两个请求一个请求篡改订单归属为他人另一个请求正常提交服务端在两个请求交错处理时可能重复校验或者漏校验。这种问题排查起来非常痛苦也容易复现不了但一旦被利用危害极大。测试时如果怀疑有竞态可以用并发工具对同一个接口发送几十个请求观察响应里有没有出现重复操作或者越权行为。4. 从攻击面到完整排查六个实操要点4.1 先画流程再标接口我在做这类测试时第一件事不是打开Burp而是先把业务流程图画出来——不是说需要多规范而是要在脑子里把“步骤”和“接口”对应起来。拿报销审批来说要明确每一步调用的是哪个接口每个接口的入参是什么出参里有什么敏感的中间态信息当前用户在这个步骤里应该具备什么权限。画完之后逐个接口问三个问题第一这个接口会不会被一个完全无关的账号直接调用第二如果我把入参里的资源ID替换成别人的会发生什么第三如果我跳过前置步骤直接请求这个接口会发生什么这三个问题基本上能覆盖大部分越权路径。4.2 双账号交叉验证越权测试离不开双账号。准备两个测试账号A和B在浏览器A中登录账号A浏览器B中登录账号B。然后开始“串门”在A中走完流程第一步把请求里的资源ID替换成B的资源在A中使用B的会话令牌直接调用B的流程接口在B中尝试调用A刚刚使用过的流程接口。注意观察响应差异有时候接口返回的是空数据不是越权但如果返回了正常的业务数据、甚至操作成功的提示那就实锤了。这里有个容易踩的坑不要把“提示资源不存在”误判为安全。很多系统对越权访问统一返回“没有权限”但也有部分系统返回了真实数据的脱敏字段响应体虽然不全却已经泄露了业务信息。4.3 跳步与重放测试跳步测试的核心方法是把请求按流程顺序分成多个阶段依次截获每个阶段的HTTP请求然后在跳过前面阶段的情况下直接发送后面的请求。用Burp Suite的Repeater就可以快速操作这个过程。重放测试针对的是“一次性流程标识”的失效问题。比如找回密码流程里的token正常使用时是一次性的但服务端可能在流程未完成前允许重复使用或者token过期时间设置过长。测试时可以拿到一个有效的流程token反复重放后续步骤接口看看是否被拦截。4.4 参数污染与类型混淆很多开发在参数校验上只做“空值校验”或“格式校验”没有做“归属校验”。测试时可以对关键参数做类型混淆比如把资源的ID从字符串改为数组、JSON对象、甚至null观察后端对异常类型的处理逻辑。有时候原本的归属校验只针对单值ID攻击者传一个数组就能绕过校验直接操作多个资源你可以试着用数组格式提交观察响应是否异常。4.5 日志审计定位异常访问对已经上线的系统越权排查还能通过日志来完成。关注两类异常记录同一个用户短时间内访问了大量不同业务ID的资源比如一个普通账号在几分钟内请求了序列号连续的三位数、四位数资源ID高频访问“修改”“删除”“审核”这些写操作的接口且操作对象ID分布不连续。把这两类行为从访问日志里捞出来配合数据库审计日志看具体操作内容往往能发现真实的越权事件。顺带一提运维侧如果已经在收集审计日志可以把这类检测规则做成自动化告警比事后追溯高效得多。4.6 自动化工具能做到什么程度自动化扫描器对单点越权有一定效果但多步流程的越权基本测不出来。原因是扫描器不知道该流程哪些步骤必须具备先后关系也不知道当前登录用户和数据对象的归属关系。我个人的做法是用自动化工具完成基础的信息收集和常规漏洞扫描越权类的专项测试仍然以手工为主。如果要辅助自动化可以自己写一个基于会话切换的脚本登录两个账号批量替换请求体中的关键ID字段然后对比两个账号收到的响应。Python里用requests加ThreadPoolExecutor就可以实现一个简化版注意测试前一定在授权范围内操作。import requests api_url https://target.example/api/order/{order_id} session_a requests.Session() session_b requests.Session() # 登录两个测试账号省略登录逻辑 # session_a.post(https://target.example/login, data{user: userA, pass: ...}) # session_b.post(https://target.example/login, data{user: userB, pass: ...}) # 用A的会话获取自己的订单ID列表 my_orders session_a.get(https://target.example/api/my_orders).json() # 遍历自己的订单ID用B的会话去访问 for order in my_orders[:20]: resp session_b.get(api_url.format(order_idorder[id])) # 如果B能读到A的订单详情即存在水平越权 if resp.status_code 200 and resp.json().get(owner) ! userB: print(f[!] IDOR: order {order[id]} readable by userB)上面的代码只是一个极简演示实际系统里还要处理登录态、接口签名、反爬验证等细节。核心思想是让脚本代替人肉去反复请求大量ID把你从重复劳动里解放出来。5. 三个实战案例复盘越权藏在哪5.1 案例一审批流中的垂直越权这是一套企业内部的后台系统。流程是员工提单、直属领导审批、财务复核。我拿到一个普通员工账号正常提交了一条加班申请单。然后我看了下审批接口发现它接收两个关键参数workOrderId和actionaction取值有approve和reject。我直接调用审批接口把workOrderId填成自己的申请单action设置为approve。结果接口返回成功申请单状态变成了已审批。问题出在后端逻辑只校验了申请单是否存在、以及申请单是否处于待审批状态完全没有校验当前用户是否属于审批链路上的节点。这个问题的根源是审批节点的身份判断缺失。修复时需要把“审批人”和“待审批节点”绑定后端要能根据当前登录人ID查询他名下的待审批事项接口只接受“当前登录人待审批列表里的申请单”而不是接受任意申请单ID。5.2 案例二多步折扣流程的订单串改另一家电商平台有一个“多步领折扣”活动第一步验证资格第二步领取优惠券第三步下单。问题出在下单接口的归属校验上。它要求前端提交userCouponId后端先用这个ID查询优惠券然后校验优惠券归属这一步是有的。但后面的“批量结算”接口入参是一组userCouponId数组后端对数组里的元素只做了存在性校验没有逐个校验归属。于是可以通过一个自己的有效优惠券混入别人的优惠券ID构造请求时就很容易实现了。整个过程还涉及订单金额重算的逻辑缺陷——后端用前端传来的商品ID和数量重新计算价格但优惠金额直接用前端传来的discount字段等于把定价权交到了攻击者手里。复盘时发现这个漏洞之所以难发现是因为它在单步测试时表现正常只有在构造多步流程、越过前序校验之后才会暴露。把它修好的关键不是简单对齐接口鉴权而是把“优惠券的使用状态”和“当前订单”做一次绑定提交结算时校验所有优惠券归属当前用户且只接受从服务端计算出的优惠金额。5.3 案例三在线答题系统的“帮答”事故某在线教育平台的考试模块答题分多步第一步答题、第二步保存草稿、第三步交卷。草稿接口的入参是scheduleId它表示“某次考试的安排”后端只校验了这个scheduleId是否处于进行中没校验考试安排是否分配给当前学生。攻击者把自己的学生账号登录后拿到一个考试安排ID然后把ID替换成别人的安排ID直接调用保存接口。因为是草稿服务端甚至没有校验“是否已交卷”于是攻击者可以反复篡改别人的答题内容甚至在交卷后通过重新保存草稿来修改已交卷的答案。这里暴露了两个问题一是草稿对象没有绑定创建者二是流程状态没有做终态保护。修复方案是草稿表增加student_id字段所有读写操作强制以当前登录人为查询条件交卷后流程状态锁定任何写操作都拒绝执行。这也是多步流程里“状态机”重要性的最好例证。6. 修复与加固把每一步都当成独立接口来防护6.1 服务端状态机流程状态不信任前端多步流程的修复第一条铁律就是流程推进到哪一步、当前处于什么状态完全由服务端维护前端传来的step、status、state字段一律不采纳只作为展示用途。实现上可以引入一个流程状态机每一步完成时服务端更新状态后续接口先查状态再决定是否放行。比如找回密码流程REQUESTED已请求、VERIFIED已验证、RESET_DONE已完成。第三步接口启动时必须先查当前用户在服务端的流程状态是否为VERIFIED否则直接拒绝。状态机看似增加了开发成本但它是防止跳步攻击最有效的手段。比起在每个接口里加一堆if判断状态机的维护成本其实更低。6.2 每个步骤接口都必须独立鉴权前面提到“信任接力”是漏洞根源修复方案就是切断接力。订单详情、订单修改、订单作废——每一个接口不管是不是同一业务域都必须单独执行以下三项检查当前登录用户是否已认证Authentication操作对象是否存在Existence当前用户是否有权限操作该对象Authorization。如果一套流程里有二十个接口那就二十个接口全部做这三件事。不要因为“列表接口已经过滤过了”就省略详情接口的校验。实际开发中我见过太多次“权限校验写成公共函数但只在第一个接口调用”的情况审计测试时一抓一个准。6.3 业务对象归属绑定避免“ID裸奔”如果业务对象ID是自增的强烈建议改造为不可枚举的随机IDUUID、雪花ID等。这并不是安全控制的核心但能让攻击者无法通过简单的ID遍历来批量探测大幅提高攻击成本。请注意这只是一种缓解手段不能替代归属校验即使ID肉眼不可猜也要在接口里做权限判断。进一步地数据库层面可以把业务对象与归属用户设计成组合字段比如orders表里的user_id查询时强制带上这个条件。SQL语句写成WHERE id ? AND user_id ?而不是WHERE id ?这样即使有人拿一个不属于当前用户的orderId来查询也查不到任何数据。6.4 流程令牌与角色绑定对于跨角色协作的流程要引入“当前节点操作人”的概念。流程实例表里增加一个字段记录“当前待操作人ID”接口更新时校验当前登录人等于这个ID而不是简单地校验“是否属于某个角色”。角色绑定还有一个容易忽略的细节当流程中的审批人发生变更时旧的待操作人ID要同步失效。我曾经见过一个系统员工离职后旧的审批账号仍然能审批之前分配给他的单子原因就是流程实例里保存的待操作人ID没有随着人员变动而更新。这种问题需要后端在组织架构变更时主动触发流程实例的权限重置。6.5 审计日志与异常告警闭环最后是审计能力。每个多步流程的关键操作都要记录以下信息操作人ID、操作对象ID、操作时间、操作类型、结果、请求来源IP。这个日志不只用于事后追溯更可以实时检测异常模式。前面4.5提到过日志告警。实际落地时可以用规则引擎或者简单的定时脚本同步检测“单用户高频访问异主ID”“审批类操作权限失败次数激增”等指标一旦触发就推送给安全负责人。它能帮你在漏洞被真正利用前发现早期的探测行为。7. 常见问题与排查技巧速查7.1 测试环境一切正常线上却被打穿很多团队在测试环境测越权时发现不了问题往往是因为测试环境里只有一个账号或者所有测试数据都属于同一权限级别。越权测试必须在双账号、多角色下进行测试数据也要尽量模拟真实生产的数据分布。建议在测试环境就建立一套完整的账号矩阵普通用户A、普通用户B、管理员C、审批角色D、财务角色E覆盖各个权限层级这样才测得出问题。7.2 修复后回归测试又出现越权有一种常见情况修复了接口A的越权逻辑结果流程的另一个入口接口B又出现了同类问题。原因是多步流程的入口比较多开发只在主路径上补了校验遗漏了分支路径。回归测试时一定要把整个流程的所有入口全部过一遍包括列表页、详情页、导出功能、批量操作接口任何一个遗漏都可能让修复形同虚设。7.3 接口鉴权明明写了却还是越权能出现这种情况大概率是鉴权逻辑写在了“不该用”的地方。最常见的是鉴权写在Controller的某个拦截器里但拦截器里的逻辑是“如果请求中包含某个参数则放行”攻击者直接把这个参数带上就绕过了。排查时别只看有没有鉴权代码要看这个鉴权是否能被绕过。建议所有权限校验放在服务端的业务逻辑层而不是依赖Web拦截器针对业务请求的鉴权永远使用当前登录会话信息不要使用请求参数里的任何字段作为判断依据。7.4 工具扫描报告为零我就放心了——这是错觉商业扫描器确实能扫出很多常规问题但多步流程中的越权漏洞恰恰是传统扫描器最不擅长的一类。因为它需要理解业务语义而扫描器不懂业务。我遇到过一些团队拿着扫描报告给客户说“系统很安全”结果手工测试不到半天就拉出一个高危越权。正确的心态是扫描器只能做广度覆盖业务逻辑和越权测试一定要结合代码审计和手工测试来深度排查。尤其是新增功能、核心交易链路、涉及多角色协作的模块必须做专项测试。8. 写在最后的一点经验做了多年安全测试我最大的感受是多步流程越权漏洞的技术含量不高但隐蔽性极强。它不像SQL注入那样有统一的检测模式而是完全依赖开发人员的权限意识。测试时多问一句“这个接口换个人来调用会怎样”审计代码时多搜一下“request.getParameter”里的参数有没有参与权限判断也许就能在攻击者之前发现那个不起眼的缺口。还有一个建议送给大家修复越权漏洞不能只打补丁要从流程设计阶段就把“每一步都需要鉴权”当成默认约束而不是事后补救。把服务端状态机、归属校验、审计告警这三件事做好大部分多步流程的越权问题都能在一开始就被堵住。