多步流程中的越权漏洞剖析与修复实践
发布时间:2026/10/18 8:14:10 锦皓数字建站

前阵子做一次授权安全评估某项目的后台审批流程让我印象很深。这个系统有一条发起申请 → 业务审批 → 办结归档的三步链路前两步做得挺规范登录校验、状态控制、角色校验都到位了。问题恰恰出在最后一步审批接口只检查了用户是否登录没检查这个人是不是审批人也没检查申请单当前是否处于待审批状态。我用一个普通员工账号填上某个申请单编号直接调用审批接口返回值清清楚楚告诉我审批成功。这类问题就是典型的多步流程中的越权漏洞——不是单步接口做得不够好而是业务一旦被拆成多个接口权限校验的接力棒在某一步掉在了地上。简而言之多步流程中的越权漏洞指的是完整业务流程被拆成多个接口后部分步骤缺少对操作者身份、角色或资源归属的校验导致用户可以访问、修改或推进本不该由他操作的数据。这类漏洞在真实系统里极其常见而且危害不小。不管你是写业务代码的开发还是负责安全测试的同学这篇内容都值得花十分钟看完。说明以下所有案例均来自模拟项目和授权测试环境不涉及真实业务数据。1. 多步流程越权漏洞的高发温床1.1 先搞清楚什么是越权漏洞越权漏洞的正式叫法是访问控制失效英文里常叫 Broken Access Control 或 IDORInsecure Direct Object Reference。它指的是服务端没有正确校验当前用户有没有权利访问某个资源、执行某个操作导致用户能看到、修改、删除本不该他碰的数据。安全圈习惯把它分成两类水平越权和垂直越权。水平越权很好理解大家都是普通用户理论上只能看自己的订单结果你把订单编号改成别人的接口照样返回数据这就是水平越权。垂直越权则是低权限用户执行了高权限操作比如普通员工把某个审批单直接审批通过这种用普通身份去干管理员或审批人才能干的事就叫垂直越权。OWASP 把访问控制失效常年排在 Web 应用风险排行榜的前面原因不是它技术上有多少花样而是太容易发生了。单接口的越权还好排查参数一换就能试出来真正让人头疼的是多步流程——几个接口串起来完成一件事只要中间任何一步埋了雷整条链路都可能被利用。这里可以用酒店房卡来类比你入住酒店拿到自己房间的房卡正常只能开自己的门但如果你拿这张卡把隔壁房间的门也刷开了那就是越权。放到多步流程场景里相当于一道门分了三道锁前两道锁都验了房卡第三道锁却只是个摆设任何人的卡都能拧开。1.2 为什么多步会让风险翻倍单步骤接口的逻辑链条很短请求进来查一下当前用户是谁再判断他能不能操作这个资源能就执行不能就拒绝。这种接口即使写漏了校验测试时也比较容易发现。多步流程麻烦在状态分散在多个请求之间——第一步做完什么事第二步要基于第一步的结果继续第三步又要基于第二步的结果收尾。很多研发在处理多步流程时有一种朴素又危险的想法第一步已经校验过身份了后面的步骤用户既然能走到说明他肯定合法。于是后续接口就只认一个流程编号、申请单ID之类的标识默认能拿到这个ID的人就是流程的合法参与者。这个假设错得很彻底。因为流程ID往往是可以预测的或者可以从列表接口、分享链接、历史消息里泄露出来。攻击者只需要一个枚举、一个批量替换就能把别人的资源ID拿来用。更深层的根源有三个权限校验只看入口不看出口。整个流程只保证第一步合法后面的步骤完全不设防等于把门锁装在门框上门板却是敞开的。把前端流程进度当成了服务端状态。前端根据按钮点击顺序维护了一个流程进度后端却没有真正保存状态机只是被动接收前端传过来的步骤参数。多步骤之间缺乏防重放和幂等机制。同一个请求被拦截后重放多次或者把步骤顺序打乱后端无法识别。我见过最典型的场景是修改绑定手机号流程。用户先输入原手机号验证码再输入新手机号最后确认。第一步做得非常规范验证码校验、限流、绑定当前用户全都齐了但提交新手机号的接口只接收一个 stepId 参数服务端拿 stepId 找到一条临时记录直接把手机号改了完全不校验这个 stepId 是不是当前用户创建的。攻击者用自己手机号正常发起流程拿到 stepId再把请求里的目标账号参数替换成别人的就能把别人的手机号改掉。这种案例在我评估过的项目里遇到过不止一次。所以多步流程之所以是越权漏洞的高发地带不是因为某一处代码写错了而是因为整个流程的信任模型从一开始就建立在一根脆弱的支柱上默认中间步骤的调用者是可信的。2. 三种典型的多步流程越权形态2.1 水平越权换个ID就改掉了别人的数据水平越权在多步流程里的最常见表现就是第一步用自己的身份第二步换成别人的ID。比如一个申请退款流程页面上的第一步是选择订单订单列表来自服务端正常全是你自己的但提交退款申请时请求体里带的 orderId 是前端填的。攻击者不通过页面直接构造请求把 orderId 换成其他用户的一笔订单如果后端在退款接口里只用 orderId 查询订单而不校验归属那么别人的订单就会被发起退款。看起来是个小问题实际影响很大。订单信息能看收货地址能看发票信息能看这些都是敏感数据。更糟的是那些修改和删除类接口一旦越权成功直接就是数据篡改或业务破坏。多步流程里的水平越权往往藏得更深因为它不是单接口的问题而是第一步的 ID 传到了第二步第二、第三步没验证 ID 的合法来源。2.2 垂直越权角色校验只写在页面上垂直越权在多步流程中呈现为高权限步骤缺失角色校验。后台系统的审批动作几乎都是一个独立接口比如 POST /api/approval/audit。负责业务的研发经常默认审批按钮只有审批人看得到普通用户连按钮都看不到后端还用得着再做一次角色校验吗用不着做——如果前端是唯一的防线那这道防线基本等于没有。我见过一个项目前端确实只在审批人账号里渲染了审批按钮后端也能鉴权但鉴权的粒度是登录即可。换句话说任何登录用户都可以直接构造 POST 请求去调用审批接口。攻击者利用它可以把自己的申请单审批通过也可以把别人的申请单驳回整个审批流程形同虚设。这类垂直越权的特征是接口位于业务流程的末端或中间环节单看接口本身它只是推进一下状态没有敏感数据返回所以代码审计初期很容易被忽略。但一旦跟状态机联系起来危害就放大了——一个能任意推进审批状态、任意驳回他人申请的接口本身就是重大安全隐患。2.3 步骤跳跃与重放不按套路出牌第三种形态是步骤顺序和次数问题。多步流程的设计初衷是必须一步一步来比如重置密码必须先验证身份再设置新密码。但后端的实现常常是两个独立的接口各管各的验证身份接口校验验证码设置密码接口直接接收新密码。只要设置密码接口没有检查这个用户是否已经通过了身份验证攻击者就能跳过验证环节直接把密码改成自己设置的。步骤跳跃还有一种变体重放。多步流程中同一个提交接口如果被连续调用多次可能造成重复提交、重复扣款、重复审批。尤其是一些没有幂等设计的接口每一次调用都会新增一条数据或改变一次状态重放攻击就会放大业务流程的破坏力。防御思路也不复杂服务端维护流程状态每个操作都校验当前状态是否允许该操作关键的推进性接口生成一次性令牌只有完成上一步步骤的用户才能拿到提交类接口做好幂等控制同一笔业务多次请求只生效一次。3. 从一次告警到漏洞确认某后台审批流排查实录3.1 奇怪的状态变更操作日志里找不到审批人前阵子某项目团队找我看一个疑难问题。他们的后台系统里一批申请单在凌晨被批量审批通过但操作日志里审批人字段完全是空的。运维第一反应是数据库被拖了赶紧把GC日志、慢查询、登录日志全翻了个底朝天也没找到任何异常登录记录。我接手后没急着查库先把网关层的关键接口访问日志拉了出来筛出所有 /approval/ 开头的路径按时间倒序排列很快发现了异常一批来自普通员工账号的请求以每分钟几十次的频率打向审批接口每次都返回 200。那批请求的调用时段和申请单状态变更时间完全吻合。真正让我确定问题的是请求参数每个请求都只带了一个 auditId再加上当前登录用户名没有任何审批人角色的标记。这说明后端接口的入参设计里压根没有审批人这个概念。3.2 顺着接口路径定位权限断点我把排查链路完整走了一遍列出来给读者参考遇到类似问题可以照着做先确定受害对象把所有状态异常变化的申请单编号收集起来放到一个集合里。反查请求记录在网关日志或数据库访问日志里找到每个申请单最后一次状态变更对应的 HTTP 请求和 SQL 语句。定位接口从请求路径中拎出具体的 Controller 方法把代码翻出来看。检查权限逻辑逐行查看方法内有没有获取当前用户、有没有判断角色、有没有校验资源归属。这一步是核心很多问题都是在这里暴露的。拉一个普通账号实测在授权测试环境下构造同样的请求用普通员工 token 去审批一个他人申请单观察返回值和状态变更情况。换一种方式再测把自己创建的申请单的审批人改成别人再调用审批接口验证水平越权是否同时存在。那次排查的结果很清晰审批接口的代码省略了业务细节后逻辑骨架大概是下面这样。PostMapping(/approval/audit) public Result audit(RequestBody AuditRequest request) { Approval approval approvalMapper.selectById(request.getAuditId()); if (approval null) { return Result.fail(申请单不存在); } approval.setStatus(Status.APPROVED); approvalMapper.updateById(approval); return Result.ok(); }看到这里大家都明白了没有取当前登录用户没有判断审批角色没有校验申请单当前状态也没有任何操作者记录。这就是一个登录即可以调的审批接口。3.3 影响面评估别只看单个接口漏洞确认之后影响面评估也是关键环节。我当时没有只盯着这个审批接口而是把整个申请流程里所有接口拉了一个清单挨个检查接口路径缺失的校验风险类型/approval/create暂无入口校验齐全低/approval/submit未校验申请单属主水平越权/approval/audit未校验审批人角色垂直越权/approval/reject未校验审批人角色垂直越权/approval/withdraw未校验申请单属主水平越权检查完发现不仅是审批接口撤回接口也存在水平越权任意登录用户可以撤回别人的申请单。原因和审批接口一样查询时只用了申请单 ID没有把当前用户作为过滤条件。所以评估一个多步流程的越权影响面一定要把流程内所有可能改变状态的接口全部列出来逐个验证三件事有没有登录校验、有没有角色校验、有没有资源归属校验。缺哪个就把哪个标记为漏洞。而且如果发现了一个大概率同一套代码风格下还有其他几个顺着同样模式去翻往往能翻出一串。4. 修复方案落地状态机、属主校验与步骤令牌4.1 服务端状态机流程进度由后端说了算修复多步流程越权首先要建立服务端状态机的意识。业务流程进行到哪一步不应该由前端传一个 step 参数告诉后端而应该由后端根据数据库里的当前状态来判断。前端只能发送我想执行某个操作后端则根据状态判断这个状态是否允许这个操作。还是拿审批流程举例。一个申请单的生命周期可以定义为DRAFT草稿→ SUBMITTED已提交→ APPROVED已通过/ REJECTED已驳回→ ARCHIVED已归档在这个状态机里能审批的只有 SUBMITTED 状态已经 APPROVED 或 REJECTED 的申请单不允许再次审批能审批的人必须有审批角色。修复后的代码骨架应该长这样PostMapping(/approval/audit) public Result audit(RequestBody AuditRequest request, AuthenticationPrincipal LoginUser currentUser) { // 1. 取出申请单 Approval approval approvalMapper.selectById(request.getAuditId()); if (approval null) { return Result.fail(申请单不存在); } // 2. 校验角色只有审批人才能执行审批 if (!currentUser.hasRole(APPROVER)) { return Result.fail(无审批权限); } // 3. 校验状态机只有待审批状态才能审批 if (!Status.SUBMITTED.equals(approval.getStatus())) { return Result.fail(当前状态不允许审批); } // 4. 记录真实操作者拒绝伪造 approval.setStatus(Status.APPROVED); approval.setApproverId(currentUser.getId()); approval.setApproverName(currentUser.getName()); approvalMapper.updateById(approval); return Result.ok(); }可以看到修复后的代码比原来只多了几步判断角色、状态、操作者记录。每一步判断都有明确目的——角色判断解决垂直越权状态判断解决步骤跳跃操作者记录解决审计追踪。这三条缺一不可。4.2 对象级授权每个环节都要绑定当前用户状态机解决的是能不能做这个动作对象级授权解决的是能不能操作这个资源。前者是动作维度后者是数据维度。一个接口可能同时缺两者也可能只缺其中一个审计时要分开看。对象级授权的核心做法就一句话在查询和更新的 SQL 里强制带上当前用户的条件。比如查订单不要写成SELECT * FROM orders WHERE order_id #{orderId}而要写成SELECT * FROM orders WHERE order_id #{orderId} AND user_id #{currentUserId}这样即使攻击者把 orderId 换成别人的查询结果也是空的自然无法越权。多步流程里特别是第一步选资源、第二步操作资源的场景属主校验必须在第二步再做一次。前端可以把 orderId 传进来但后端不能信任它的归属必须重新校验。有些研发觉得用 UUID 做资源 ID 就安全了反正别人猜不到。这个想法经不起推敲UUID 虽然难枚举但可以从列表接口、日志、分享链接、浏览器历史里泄露出来。安全设计的原则是把每个请求都当作未认证请求去校验而不是赌参数不可猜测。4.3 步骤令牌与幂等控制堵住跳跃和重放状态机能够挡住步骤跳跃但还不能完全防住重放。如果某个操作接口被连续调用两次两次都满足状态条件还是可能重复生效。更稳妥的做法是引入步骤令牌。简单说服务端在完成某一步骤后生成一个一次性令牌返回给前端只有带着这个令牌才能调用下一步接口。令牌的内容可以绑定当前用户、资源ID、步骤编号和过期时间达到防止跨用户使用、防止跨资源使用、防止超时重放的效果。实现不复杂用签名串或短时缓存都可以// 生成步骤令牌绑定用户、资源、步骤、过期时间 String stepToken SignUtil.sign( currentUser.getId() : approvalId :step2, serverSecret, expireAt );配合幂等设计就更完整了。对提交审批支付确认这类关键接口用业务流水号加唯一约束重复请求命中唯一索引直接返回上次结果而不是重复执行。一次审批操作只能生效一次这是底线。修复方案的优先级也要说清楚第一优先级是状态机加对象级授权这两个不做好其他都是空谈第二优先级才是步骤令牌和幂等属于加固项。我在实际项目里见过一些团队为了防越权给每个步骤都套上令牌却连最基本的角色校验都没做等于开着大门给窗户上锁。5. 把越权漏洞堵在开发阶段审计清单与测试思路5.1 代码审计时重点盯住哪几类接口与其等漏洞被外部攻击者发现不如在代码评审阶段就把隐患查出来。我把这些年审计接口的经验浓缩成一张检查清单供参考凡是接收 ID 参数的接口。orderId、userId、flowId、auditId、addressId、fileId看到了就多问一句查询条件里带当前用户了吗更新条件里带当前用户了吗删除接口呢。凡是能改变状态的接口。create、submit、approve、reject、close、archive、resend、confirm这些是流程关键节点逐个检查角色和状态机校验。凡是返回列表的接口。列表接口是批量数据泄露的高发区除了检查属主条件还要检查分页参数是否被滥用。凡是管理后台的接口。后台接口不能只判断登录即通过必须校验具体角色或权限点且要用方法级权限注解兜底。凡是涉及用户身份切换的接口。比如模拟登录、切换账号、找回密码、修改绑定手机号这些接口一旦缺失校验后果就是账号接管。审计的时候不要只看单个方法要把整个流程串起来看。把流程涉及的所有接口按执行顺序排列标记每个接口的鉴权情况一眼就能看出断点在哪里。很多时候第一、第二步做得严严实实第三步却空门大开根源就是研发把精力放在流程开头忽略了流程走向深处后的校验延续。5.2 自动化扫描与手工测试怎么配合自动化扫描对越权漏洞的覆盖率比很多人预期的要高能发现相当一部分问题。常见做法是用爬虫把接口和参数收集起来登录两个普通账号 A 和 B拿 A 的资源 ID 放到 B 的 token 下去请求观察响应码和响应体是否泄露数据。现在不少开源工具可以直接落地这类测试。但自动化有两个盲区。第一纯自动化的 ID 替换只会测水平越权测不了垂直越权——它不知道某个接口只应该由审批人角色调用。第二自动化不会主动把流程乱序执行。手工测试要补两件事一是把流程的每个接口打乱顺序调一遍比如不提交直接审批看后端是否拦截二是对高权限操作接口用一个低权限账号直接请求确认返回结果。这两步不需要多复杂的工具一个能改请求的工具加上一点耐心就够。我个人的习惯是评估一个多步流程时永远先走一遍正常流程把所有响应里的 ID、token、step 都记录下来然后再用这些值去替换其他账号的资源。多步流程的越权往往藏在正常流程产生的 ID 被二次利用这个环节里。5.3 团队层面怎么让这类漏洞不再反复出现单个漏洞修复了不代表问题不会再出现。只要团队没有形成统一机制下一个项目里大概率还会写同样的代码。我观察下来能长期管住越权漏洞的团队通常做到了以下几点一是把权限校验组件化。封装好 requireLogin、requireRole、checkOwner 这类注解或中间件业务代码里直接标记减少手写鉴权逻辑的机会。二是把接口的角色和资源范围写进设计文档。评审时至少过一遍谁可以调用、可以操作哪些资源、当前状态是什么。三是接入自动化测试。把越权用例固化成接口测试脚本进 CI 之前跑一遍从机制上拦住回归。四是定期专项检查。每个季度或半年集中对核心业务的多步流程做一次越权专项评估而不是等到出了问题再救火。最后补充一点我自己的感受越权漏洞不会因为换了个框架、升级了网关就消失它本质上是业务逻辑的问题跟技术栈无关。把多步流程的每一步都当成独立的受保护接口来设计权限校验不是在入口做一次就算完而是每走一步都重新确认一次。看起来多做几道判断实际挽回的都是潜在的批量数据泄露事故。这些做下来多步流程里的越权漏洞会从高发变成偶发最终变为罕见。我在实际评估中最大的体会是绝大多数越权漏洞不是安全团队没测出来而是研发团队在设计流程时根本没把每一步都要鉴权当成默认前提。把这个前提焊死在团队的开发习惯里比任何单个修复方案都管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。