news 2026/10/13 18:08:41

多步流程越权漏洞:从原理剖析到检测修复实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多步流程越权漏洞:从原理剖析到检测修复实战指南

权限滥用几乎是漏洞清单里最容易被低估的一类,尤其是埋在长流程里的越权漏洞,往往要在特定步骤组合下才触发。梳理一个典型的订单业务:整个取消订单闭环分四步——选择地址、确认商品、填写发票、提交取消申请。前三步都正常,第四步后端只认请求体里的orderId,压根没校验这个订单是不是当前登录用户自己的。结果一个低权限账号把别人的订单直接取消掉,还能看到订单金额和收货地址。这类问题在测试报告里常写成“多步流程越权”,本质是权限管理在步骤之间出现了断裂。

这篇文章写给两类人:一类是做安全测试、想系统排查业务逻辑漏洞的同学,另一类是后端开发,想搞清楚为什么自己写的接口会被人跨用户操作。我会把越权原理、多步流程里的攻击视角、手工检测工序、修复方案和排查坑位一次性讲清楚,内容偏实操,尽量不堆概念。你可以直接拿这套思路去梳理自己的项目,也可以把它当成代码评审时的检查清单。

1. 越权漏洞的本质:为什么多步流程里藏着更多问题

很多人第一次接触越权时,都是从单个接口的“替换ID试试”开始的。比如把一个请求里的userId从1001改成1002,发现返回了别人的资料,于是判定为越权。这种单步越权确实好抓,但真正让人头疼的是那种需要走完三步、四步才触发的问题,因为每一步单独看都“没问题”,合到一起就成了一个大洞。

1.1 水平越权和垂直越权,先分清再往下聊

越权通常分成两类,多步流程里两类都可能出现,而且经常叠加。

对比项水平越权垂直越权
核心对象同级普通用户不同权限角色
典型体现查看、修改、删除其他人的数据普通用户调用管理接口
多步流程表现后置步骤没有校验资源归属跳过审批步骤直接执行最终动作
危害侧重数据泄露、数据篡改越权操作、权限提升

水平越权最经典的形态是不安全的直接对象引用,也就是常说的IDOR。前端传了一个资源ID,后端拿这个ID直接查库,没有校验它是不是属于当前登录用户。垂直越权则更直白,一个普通账号可以访问到需要更高角色才能执行的接口,比如发起一笔提现,正常流程要经过主管审批,但攻击者直接调用了提现执行接口。

给你一个生活化的类比:想象一栋带三道门禁的楼,第一道门卫认工牌,第二道电梯按楼层,第三道房间钥匙。水平越权就是你拿自己的卡刷开了别人家房门,垂直越权就是你绕过电梯权限控制,直接从天台溜进顶层办公区。多步流程相当于把这三道门拆成了三个独立HTTP请求,任何一个环节少校验一把钥匙,整栋楼就漏了。

1.2 多步流程的问题不在“步骤多”,而在信任边界错位

开发者在写多步流程的时候,脑内逻辑通常是线性的:第一步验证登录态,第二步验证资源归属,后面就默认“这个用户已经通过了”。这种默认在单体页面上也许还能成立,但在接口满天飞的移动端和前后端分离架构里,每一个请求都是独立抵达后端的,后端只能根据当前请求里的参数来判断该做什么。

问题就出在这里:只要中间某个接口把“归属标识”当作客户端传入值来信任,越权就出现了。比如修改手机号流程里,第一步校验旧密码,第二步提交新手机号。如果第二步的请求体里可以额外传一个userId,而后端只用这个userId来定位要改哪个账号,那么攻击者把自己的userId换成受害者的,就能在不知道受害者旧密码的情况下改掉对方手机号。

再往深一层看,多步流程还容易出现“步骤间状态缺失”。前端靠页面跳转来维持流程顺序,但后端没有保存任何“我走到第几步了”的状态。攻击者完全可以跳过第一步,直接发送最后一步的完整请求。在没有流程状态校验的接口面前,按钮上写的“下一步”只是一个装饰。

1.3 越权为什么比参数注入更让业务头疼

SQL注入、命令注入这类漏洞,通常一个补丁就能封死。越权不一样,它长在业务逻辑里,每个流程的校验方式都不一样,很难用一套通用规则覆盖。尤其是多步越权,它不像单个参数漏洞那样扫一遍就能发现,往往需要组合多个步骤才触发,自动化工具基本帮不上忙。

业务上的后果也分几个层级。轻一点的是水平越权泄露他人隐私,比如姓名、电话、住址、订单记录;重一点的是越权执行操作,比如取消订单、修改收货地址、发起退款、改账号凭证,直接造成资金或资产损失。垂直越权则可能让一个普通用户调用到管理员才能使用的接口,后续的破坏面就完全不可控了。所以这类漏洞在正式风险评估里通常被定得很高,因为它直接踩中了数据完整性和业务逻辑这两个关键点。

2. 多步流程越权的典型场景与攻击视角

要理解怎么利用,先得理解攻击者的思考方式。多步流程在设计和实现上各有不同,但攻击者盯上的关键点就那么几个:资源主键、流程状态、角色字段。下面我把常见的攻击视角拆成可操作的思路,再结合几个业务场景讲清楚。

2.1 前端流程只是“纸老虎”,接口才是真正的攻击面

前端把流程做成多步,本意是降低用户误操作率,给每个步骤一个确认机会。但对攻击者来说,前端只是一个可以跳过的界面。用代理工具抓一遍正常操作,就能拿到一串接口路径、请求参数、Cookie和响应结构。比如一个“找回密码”流程,表面上要填手机号、收验证码、设新密码,实际接口可能就是/api/password/reset/verify和/api/password/reset/confirm两个。

攻击者不需要一个步骤一个步骤地点按钮,他可以直接把confirm这个接口翻出来看参数。如果确认接口只接收code和newPassword,而后端没有把code和具体用户绑定,那这就是一个多步流程里的水平越权:任何人都可以用一个已知验证码改他的密码。理解了这个视角,后面所有检测思路都顺了——永远不为“前端限制了输入”而放心,只看服务端到底校验了什么。

2.2 攻击者视角:跳过、替换、篡改

在具体的多步流程里,攻击者通常做三件事。

第一,跳过前置步骤。正常流程是A到B再到C,攻击者直接调C。如果C接口没有校验前置状态,比如没有验证码校验记录、没有审批通过记录、没有上一步生成的临时会话ID,那流程就是虚设的。

第二,替换关键ID。这是最常见的手法。流程里某个请求携带了资源ID,比如订单ID、文件ID、用户ID,攻击者把它换成别人的ID,看后端是否还能正常返回或执行。替换的ID来源可能是数据包里的明文参数,也可能是JSON里的嵌套字段,先把所有id、uid、target、resource这类字段都标记出来。

第三,篡改状态与角色字段。有些系统会把流程状态、用户角色也放在客户端参数里,比如"step": 3、"role": "admin"、"approved": true。这类字段一旦交给前端控制,等于把流程安全拱手送人。

这三步可以组合使用。真实攻击中,攻击者往往先正常走一遍拿完整接口链,再针对每一个包含资源标识或状态标识的请求做变异测试,最后根据响应差异判断漏洞是否成立。

2.3 四个业务场景里最容易被利用的多步越权

第一个场景是订单状态流转。以电商为例,取消订单正常是“查订单→填原因→确认取消”。后两步的接口往往不是同一个,确认取消的接口接收orderId和cancelReason,然后直接把订单状态改成已取消。如果后端只校验了“订单存在”和“订单状态是待支付/待收货”,却没有校验orderId属于当前登录用户,那就意味着登录用户可以取消任意人的订单。类似的问题还会出现在修改收货地址、申请退款、查看物流轨迹等子流程里。

第二个场景是账号安全设置。改绑定手机号、改邮箱、改密码这类流程,天然是多步骤的,因为要先验证旧凭证。典型漏洞是第一步验证完旧密码后,后端返回一个verified=true的标记,第二步接口直接信任这个标记。可这个标记如果只是客户端可读的响应字段,攻击者完全可以伪造。更隐蔽的写法是在第二步请求体里放userId,后端用它来定位账号,这就回到了IDOR的老路上。

第三个场景是文件上传与转存。很多系统先让用户把文件传到临时目录,再由用户确认后“转存”到正式目录。转存接口如果接收一个fileToken或tmpFileId,却没有核对这个临时文件是谁传的,攻击者只要拿到别人的token就能盗取文件内容。我见过一个相册应用,上传预览图之后可以“设为封面”,封面接口只校验图片ID,最后硬生生被人遍历出了一整站的用户头像原图。

第四个场景是审批流与工作流。发起申请、主管审批、执行操作这三个动作,在系统里通常是三个独立接口。审批接口有权限校验,但执行接口如果只校验“任务存在”就继续往下跑,攻击者可以先正常发起一条申请,拿到任务ID,再绕开审批接口直接调用执行接口。垂直越权往往就藏在这种“最后一公里”的位置。

3. 检测与复现:多步越权手工测试的完整工序

手工测多步越权,最忌讳上来就拿工具扫参数。规矩是先完整走一遍业务流程,把接口清单和参数关系摸清楚,再针对每个关键点做定向验证。整个过程不复杂,但需要耐心。

3.1 第一步:先把流程拆成“接口清单”

用抓包代理或者浏览器开发者工具,把一个正常用户从入口到结束的所有请求记录下来。不要只关注页面展示的内容,要把每个请求的方法、路径、参数、请求头、返回值都存下来。特别是那些在流程“中间节点”发生的请求,它们往往承担了状态校验和资源ID传递的任务。

举个例子,一个修改绑定手机号的流程可能长这样:

  1. 登录后请求发送验证码到旧手机号:POST /api/security/send_code
  2. 提交旧手机号验证码:POST /api/security/verify_old
  3. 发送新手机号验证码:POST /api/security/send_new_code
  4. 提交新手机号并完成绑定:POST /api/security/bind_new

手工测试时可以先用命令行工具直接重放其中一个请求,体会一下去掉前端之后,每个接口单独面对攻击者时是什么状态。

curl -i -X POST 'https://app.example.com/api/security/bind_new' \ -H 'Cookie: session=your_session_id' \ -H 'Content-Type: application/json' \ -d '{"userId": 10001, "newPhone": "13800000000", "newCode": "123456"}'

这一步不急着看漏洞,先把接口的“可重复调用性”和“参数独立性”摸清楚。很多后端接口即使前端流程没走完,单独调用仍然有效,这本身就是流程设计上的风险点。

3.2 第二步:给每个接口做一次“归属审计”

把接口清单整理完之后,逐个问下面几个问题。

  • 这个接口最终操作了数据库里的哪张表?
  • 关键资源的主键来自路径、查询参数、请求体,还是来自服务端会话?
  • 服务端有没有显式取出当前登录用户,并用它过滤资源?
  • 有没有只校验“资源存在性”,而没校验“资源归属”?
  • 流程状态是存在服务端,还是由客户端通过参数传递?

这些问题里,最重要的就是最后两个。我见过太多接口在代码开头写了一句“if order exists”就继续往下走,却完全没有判断“order.owner_id == current_user.id”。这种代码在单个接口评审时很难发现,但组合进一个多步流程里,就成了越权的通道。

如果你有源码权限,直接搜findById、getById这类方法,再往上层看有没有调用鉴权函数。如果没有源码,就靠请求参数和响应差异来判断。凡是响应里带着完整业务数据的接口,都值得重点关注。

3.3 第三步:双账号交叉验证,别用单账号自嗨

单账号测试最大的问题是“看起来一切正常”,因为校验逻辑可能针对当前用户是通的,只有换成别人的资源ID才会露出马脚。正确的做法是准备两个账号A和B,用A走完整个流程,然后把关键请求里的资源ID替换成B的资源,再观察返回是否仍然成功。

具体操作步骤是这样:

  1. 用A账号登录,正常走到流程的倒数第二步,记录下最后一步请求的完整数据包。
  2. 另起会话登录B账号,从业务里找到一条属于B的资源,比如订单号、文件ID或用户ID。
  3. 把A的请求包里的资源ID改成B的,其余内容不要动,重新发送。
  4. 对比A和B会话下的响应内容。如果B尝试访问A的资源时,后端返回了正常数据或成功状态,说明水平越权成立。
  5. 再单独测试“跳过前置步骤”:不执行流程前几步,直接发送最后一步的请求,看后端是否因为缺少验证记录而拒绝。

两个测试账号最好用完全不同的手机号、邮箱注册,避免服务端误按手机号判断归属。我习惯在请求里加一个随机参数,比如?debug=random123,防止浏览器或代理缓存干扰判断。

3.4 用脚本批量跑一遍,别靠肉眼硬看

如果业务里资源ID是纯数字且存在递增规律,手工一个一个换ID效率太低。这时候可以写个简单的脚本,遍历一批ID,再对比响应状态码、返回体长度或者特定字段值,快速定位可疑的越权点。下面这个例子用Python的requests库演示了思路,务必在授权测试环境下运行。

import requests s = requests.Session() s.post("https://app.example.com/api/login", json={ "username": "user_a", "password": "test_password" }) # 这里填一个目标范围内的订单ID列表 order_ids = [10001, 10002, 10003, 10004, 10005, 10006] for order_id in order_ids: r = s.post("https://app.example.com/api/order/cancel", json={ "orderId": order_id, "reason": "自动化越权验证" }) print(order_id, r.status_code, len(r.content), r.text[:120])

如果脚本跑下来,发现多个不同ID都返回了相同的成功状态码和相近的响应长度,那通常意味着后端根本没有按归属过滤。不过要注意,这个脚本一旦执行就是真实业务操作,所以请只在测试环境或自己创建的数据集上跑,改状态类接口务必慎用。

4. 修复方法:把校验搬回服务端,锁死流程状态

检测出漏洞只是第一步,真正值钱的是修复方案。多步越权的修复没有一个万能补丁,但有几条工程化的原则可以覆盖绝大多数场景。

4.1 统一“归属校验”原语,写在所有变更操作之前

最好的办法不是让每个开发自己写if判断,而是封装一个统一的归属校验函数,所有涉及资源ID的接口在动手改库之前都必须调用它。伪代码大概长这样:

def assert_owner(model, resource_id, current_user_id): obj = db.session.query(model).filter_by(id=resource_id).first() if obj is None: raise ResourceNotFound() if obj.owner_id != current_user_id: raise PermissionDenied()

订单接口调用时传Order模型,文件接口传File模型,用户资料接口传UserProfile模型。这个函数同时覆盖了“资源不存在”和“资源不属于当前用户”两种情况,避免攻击者通过不同报错文案来探测数据是否存在。关键点是不能只查resource_id,一定要连带owner_id一起查出来再比对。

4.2 用一次性流程令牌锁住步骤顺序

针对“跳过中间步骤直接调最后一步”的问题,比较成熟的方案是引入一次性流程令牌。第一步校验通过后,后端在Redis或内存里生成一个随机token,并保存它的状态、用户、目标和过期时间,然后把这个token返回给前端。后续的每一步请求都必须携带这个token,后端先验证token是否存在、是否匹配当前用户、状态是否处于正确的前置节点。

# 伪代码:第二步校验流程令牌 flow = redis.get(f"flow:{token}") if not flow: abort(403) if flow["user_id"] != current_user.id: abort(403) if flow["step"] != "old_phone_verified": abort(403)

token在流程成功完成后立刻删除,任何一步失败也删除,不允许重复使用。这样一来,攻击者即使拿到了最后一步的接口地址,缺少token也什么都干不了。注意token一定要用高熵随机串,别用自增数字或者可预测的时间戳。

4.3 会话承载上下文,别再让前端传“我是谁”

多步流程越权里最普遍的一个坏习惯,就是把当前用户、当前角色、当前步骤这类“上下文信息”放进前端可控的请求体里。正确的做法是:当前用户永远从服务端登录态解析,前端只传操作目标,比如orderId、fileId,并且后端拿到目标ID之后,立即用当前用户的身份去过滤它。

比如“修改订单收货地址”的接口,用户只需要传orderId和newAddress,后端内部再跑一遍SELECT * FROM orders WHERE id = ? AND user_id = ?,查不到就直接抛异常。这就把“这个订单是否属于当前用户”的责任完全留在了服务端,而不是交给前端自觉。

4.4 自动化防线:代码扫描与接口回归测试

修复之后还需要防止问题再次出现。代码扫描层面,可以给静态分析工具加一条规则:凡是接口函数中使用了请求体里的ID字段作为查询条件,且函数体内没有调用鉴权或归属校验函数,就标记为风险。虽然会有误报,但能逼着开发人员思考“这个ID能不能被客户端随便改”。

接口回归测试层面,把双账号交叉验证固化成自动化用例。每个涉及资源ID的核心接口,都用两个不同账号跑一遍“A账号操作B账号资源”的用例,断言返回403或业务失败。这一步投入不大,但对防止回归非常有效。团队规模允许的话,还可以在数据库层面做行级安全策略,让所有查询默认带上当前用户的条件,作为纵深防御的最后一层。

5. 常见踩坑与排查技巧实录

测试和修复多步越权的时候,总有几个坑反复出现。有的是开发习惯导致的,有的是测试环境干扰导致的,下面是几个我遇到过的典型问题。

5.1 最容易被忽略的“前置校验假象”

有一种代码长得很安全:第一步校验旧密码,通过了才允许走第二步。但实现上,第一步通过后并没有在后端保存可信状态,而是给前端返回了一个verified=true的字段。第二步接口的逻辑是“如果请求体里verified为true,就允许修改”。攻击者直接在第二步的请求体里填verified=true,等于骗过了整个流程。

判断一个校验是否真实,就看它依赖的数据到底由谁产生。服务端自己保存在会话或数据库里的状态才能信,客户端能改的字段都不能作为安全依据。在代码评审时,看到if request.form.get("verified") == "true"这种写法,可以直接判定为高危。

5.2 缓存和重复请求带来的误判

检测过程中,最大的干扰因素不是逻辑,而是缓存。浏览器缓存、代理缓存、CDN缓存都可能让你看到一套和实际逻辑不符的响应。比如你替换ID后返回了一个疑似别人的页面,结果可能只是某个GET接口被缓存了,而不是越权。处理方法是给测试请求加随机查询参数,清空缓存之后再验证,同一场景重复请求三次以上,结果一致才下结论。

POST接口通常不会被缓存,但一些系统用了自定义的响应缓存中间件,所以不要默认它不会缓存。另一种干扰来自并发重复请求:你连续发了两次取消订单请求,第一次成功,第二次因为订单已取消而返回失败,这可能被误判为“第二次是越权失败”,实际上只是业务流程本身不幂等。

5.3 为什么移动端接口比网页端更容易出现多步越权

多步越权在移动端App里出现的频率明显比传统网页端高。原因挺现实:App屏幕小,一个复杂流程往往被拆成四五个小接口,而且很多接口是给“内部复用”设计的,鉴权字段反而不如Web端完整。加上不少移动端接口为了省流量会返回精简字段,前端状态靠本地存储维持,后端如果不做校验,流程就是全裸的。

我对比过同一套业务在Web端和App端的接口实现,Web端走页面跳转、每步都有权限检查,App端因为要做交互优化,把校验逻辑挪到了前端控制器里。结果一个订单查询接口在Web端是安全的,换成App端的接口直接可以用任意订单号查别人信息。所以在测试或做安全评审时,不要只盯着Web端,移动端接口更应该逐条审计。

5.4 排查技巧速查表

现象可能原因排查方法修复要点
多步流程只走最后一步也能成功后端没有保存流程状态跳过中间步骤直接重放末步请求引入一次性流程令牌,服务端校验状态
替换ID后仍能访问他人资源资源归属校验缺失双账号交叉替换关键ID增加统一归属校验,连带owner_id比对
响应里带他人的字段数据查询未按当前用户过滤检查接口是否使用了请求体ID直接查库用session解析当前用户,再过滤资源
前端返回verified/role标记客户端参数参与安全判断修改请求体里状态字段看是否生效安全状态只保存在服务端会话
同一接口不同用户返回差异归属校验不完整或缓存干扰加随机查询串重复验证统一校验逻辑,并清理缓存

这张表是我每次复盘越权问题时的检查模板,你也可以直接抄走,遇到类似现象先对号入座,再深挖具体实现。

最后说点个人体会。多步流程里的越权,往往不是某个程序员有意留下后门,而是因为大家默认“上一步验证过,这一步就不用再查”。可HTTP请求本质上是无状态的,前端流程再漂亮,也管不住攻击者直接拿报文来问。我在实际项目里处理这类问题,第一反应不是教开发补两个if,而是先把流程状态机画出来,看看哪些校验被放在了客户端、哪些参数可以随意更改,先把信任边界理顺,再谈加防御。这个思路,比到处打补丁有用得多,也希望对你接下来的排查和开发工作有帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 18:08:35

用NumPy手写神经网络:从反向传播到MNIST分类实战

1. 核心设计:为什么用NumPy手写神经网络接触深度学习这么久,我一直觉得有一件事是绕不过去的:你天天用框架调接口,但真的让你从零推导一遍反向传播,能把每个参数的梯度算对的人并不多。这个项目用纯NumPy在MNIST上实现…

作者头像 李华
网站建设 2026/10/13 18:08:30

多机器人协同避障MATLAB仿真平台搭建实战指南

简介:一套基于MATLAB的多机器人路径规划与协同避障仿真平台,面向人工智能、机器人学及智能交通方向的研究者与本科高年级学生,可用于验证图搜索、遗传算法、粒子群优化等算法在多智能体系统中的路径搜索与避障效果。资源共80个文件&#xff0…

作者头像 李华
网站建设 2026/10/13 18:07:15

AI编程助手越用越笨?从无状态模型到AGENTS.md的Memory工程实践

最近一段时间,我越来越频繁地听到一句话:“AI 编码助手越用越笨。”明明昨天还讨论好的模块划分,今天换个会话就全忘了;上个月定下的命名规范,它总是按照自己的习惯来。如果你也有同感,那大概率不是模型能力…

作者头像 李华
网站建设 2026/10/13 18:06:41

Qt Creator实现高可靠MODBUS TCP客户端实战

简介:本资源是一个基于Qt Creator开发的MODBUS TCP主站客户端完整工程,面向工业自动化领域初学者与嵌入式/上位机开发工程师,解决Modbus设备远程数据采集与可视化显示的实际问题。项目采用Qt 5.9跨平台框架,涵盖网络连接、协议请求…

作者头像 李华
网站建设 2026/10/13 18:05:32

商家联盟会员消费管理系统:老技术栈的架构与部署实战

这套系统,我刚接手的时候第一反应是“老”:ASP.NET 2.0 的 WebForms、SQL Server 2005、IIS 6/7 的部署方式,每一项拎出来都像是上一代的东西。但真正把它跑起来、用起来之后我才发现,老技术栈恰恰是把业务讲得最透的一套方案。商…

作者头像 李华
网站建设 2026/10/13 18:03:47

SpringBoot+Vue+MyBatis实现企业级家电销售管理平台

在家电行业摸爬滚打了几年,从早期用Excel表格管库存、手写销售单的日子,到后来参与搭建真正能支撑多门店、多角色协同的销售管理平台,我对这类系统的痛点和价值体会很深。今天要聊的这套“企业级家电销售展示平台管理系统”,基于S…

作者头像 李华