越权访问这个问题,我在做安全评审和渗透测试的几年里见过太多次了。它不像SQL注入那样有明显的报错回显,也不像XSS那样能直接在浏览器里弹窗验证,很多时候一个越权漏洞就藏在一个看起来平平无奇的接口参数里,开发觉得"这功能本来就这样",测试觉得"点得通就行",结果上线之后被人拿一个改过的ID就把别人的数据全拉走了。这篇总结我想把越权访问这件事从头到尾捋一遍——它到底是什么、为什么这么容易出、常见的有哪几种形态、怎么系统性地去发现和验证、以及修复的时候有哪些容易踩的坑。不管你是刚接触Web安全的新手,还是已经做过一段时间代码审计或渗透的从业者,应该都能从里面找到能直接拿去用的东西。
1. 越权访问到底在越什么:先把概念的地基打牢
很多人对越权的理解停留在"改了URL里的id就能看别人数据",这个理解不算错,但太窄了。要真正把这类漏洞吃透,得先搞清楚它本质上破坏的是什么。
1.1 从"身份认证"和"权限校验"的分界线说起
一个请求进入系统,通常要过两道关。第一道是身份认证(Authentication),回答的是"你是谁"——你的session有效吗、token对不对、密码有没有过期。第二道是权限校验(Authorization),回答的是"你能干什么"——你这个身份,有没有资格访问这个资源、执行这个操作。
越权访问的漏洞,几乎全部出在第二道关上。也就是说,攻击者往往是已经合法登录的用户,他手里的凭证是真的,身份认证这一关他是过了的。问题在于系统在认证之后,没有认真去判断"这个已登录的用户,到底该不该碰这条数据"。这就好比一栋大楼,门禁刷卡你刷进去了(认证通过),但进去之后每个房间的门都没锁,你可以随便推开任何一扇门(权限校验缺失)。
这个区分非常关键,因为它直接决定了你排查漏洞时的思路:不要去纠结登录逻辑本身,而要盯着"登录之后,每一个涉及资源读写的接口,有没有做归属判断"。
1.2 水平越权和垂直越权:两种完全不同的破坏面
业内通常把越权分成两大类,这个分类不是学术上的洁癖,而是因为它们的成因、危害和修复方式都不一样。
水平越权(Horizontal Privilege Escalation),指的是同级别用户之间的越权。A用户和B用户都是普通会员,权限等级一样,但A通过某种手段访问到了B的私有数据。典型场景就是订单详情、个人资料、私信记录这类"只应该属于某一个人"的资源。这类漏洞的危害面往往最大,因为它影响的可能是全站所有用户,一个遍历就能把整个用户表的数据拖出来。
垂直越权(Vertical Privilege Escalation),指的是低权限用户访问高权限功能。普通用户去调管理员才该调的接口,比如删除用户、修改系统配置、查看后台统计。这类漏洞的成因通常是"前端隐藏了入口,但后端接口没做角色校验"——菜单里看不到"删除用户"按钮,但你把那个请求抓出来重放,照样能删。
下面这张表把两者的核心差异摆在一起,方便对照记忆:
| 维度 | 水平越权 | 垂直越权 |
|---|---|---|
| 越权主体 | 同级用户A访问同级用户B的资源 | 低权限用户访问高权限功能 |
| 典型资源 | 订单、资料、私信、文件 | 后台管理、系统配置、批量操作 |
| 常见成因 | 只校验登录不校验归属 | 只做前端隐藏不做后端角色校验 |
| 危害范围 | 可能波及全站同类用户 | 通常影响系统整体安全 |
| 检测重点 | 资源ID的可预测性与归属校验 | 接口的角色权限矩阵是否完整 |
理解了这个分类,你在实际测试时就能有意识地分两条线去走:一条线是拿两个同级账号互相打,看能不能串数据;另一条线是拿低权限账号去够高权限接口。
1.3 为什么越权比注入类漏洞更"隐蔽"
我个人的体会是,越权漏洞的隐蔽性来自三个层面。第一,它没有异常特征。一个越权请求在服务端看来是完全合法的——身份是真的、参数格式是对的、返回也是正常的200,日志里看不出任何异常。第二,它依赖业务语义。判断一个请求是否越权,必须知道"这个资源属于谁",而这个信息往往只存在于业务逻辑里,通用的安全设备很难自动识别。第三,它常常被"功能正常"掩盖。开发自测时用自己的账号点一遍,功能是通的,就认为没问题了,根本不会想到换个ID会怎样。
正因为隐蔽,越权才成了实战中产出比极高的漏洞类型。很多SRC(安全响应中心)项目里,一个水平越权就能直接拿到高危甚至严重评级,因为它往往意味着批量数据泄露。
2. 越权漏洞的高发场景:从真实业务形态里找规律
光知道概念不够,得知道它一般长在什么地方。我把这些年见过的高发场景归了几类,基本覆盖了绝大多数实际案例。
2.1 基于资源ID的直接对象引用
这是最经典、也最常见的一类。接口设计成/api/order/detail?id=10086这种形式,服务端拿到id直接查库返回,完全不判断这个订单是不是当前登录用户的。攻击者把id改成10087、10088,就能把别人的订单一条条读出来。
这类问题的根源在于接口把"资源标识"当成了唯一的查询条件,而把"当前用户"这个维度丢掉了。正确的做法应该是查询条件里同时带上用户身份,比如where order_id = ? and user_id = ?,让数据库层面就保证只能查到自己的数据。
我见过一个特别典型的案例:某系统的发票下载接口是/invoice/download?no=xxx,发票号是递增的。测试的时候随手把no减一,直接下载到了另一个用户的发票PDF,里面姓名、税号、金额全都有。这种就是纯粹的直接对象引用没做归属校验。
2.2 前端隐藏但后端裸奔的功能入口
垂直越权里最常见的就是这种。管理后台的菜单是根据角色动态渲染的,普通用户登录进去看不到"用户管理"这一项,于是开发就认为"普通用户访问不了"。但实际上,只要你知道/admin/user/delete这个接口存在,用普通用户的cookie直接请求,照样能执行。
这里的认知误区是把"看不见"等同于"访问不了"。前端隐藏只是体验层面的东西,任何人打开开发者工具、抓个包、或者直接构造请求,都能绕过。真正的权限控制必须落在后端每一个接口上。
排查这类问题的思路很直接:拿到一个低权限账号,把高权限账号能触发的所有请求抓下来,逐个用低权限cookie重放,看哪些还能成功。这个"请求重放"的动作,是垂直越权检测的核心手法。
2.3 参数污染与身份字段可篡改
有些系统为了"灵活",会把用户身份也放在请求参数里传,比如POST /api/getUserInfo带上{"userId": 123}。如果后端直接信任了这个userId,而不去和session里的真实身份做比对,那就等于把越权的钥匙直接交给了用户。
更隐蔽的一种是参数覆盖。比如请求里本来有userId=123(当前用户),攻击者再追加一个userId=456,某些框架在解析时会取最后一个值,导致身份被悄悄替换。这类问题在参数绑定、JSON解析、以及一些老框架里都出现过。
还有一种情况是身份字段藏在Cookie或Header里且可预测。比如用uid=123这种明文cookie标识身份,改一下数字就变成了别人。这严格说也属于身份认证的范畴,但表现出来的效果就是水平越权。
2.4 批量接口与导出功能:越权的放大器
单个详情接口越权,一次泄露一条;但批量查询和导出接口越权,一次可能就是几万条。我特别想强调这类场景,因为它的危害被严重低估了。
比如一个"导出我的订单"功能,接口是POST /export,参数里带一个orderIds数组。如果后端对数组里的每个id都不做归属校验,攻击者塞进去一批别人的订单id,就能一次性导出大量数据。再比如列表接口GET /api/list?page=1&size=100,如果这个列表没有按当前用户过滤,那分页遍历一遍就是全量泄露。
这类接口的检测要点是:看它是否支持传入资源ID集合,以及集合里的每个元素是否都被校验。很多系统只校验了第一个,或者干脆一个都没校验。
3. 系统化发现越权:一套可复现的检测流程
知道了场景,接下来讲怎么系统地去找。零散地瞎点效率很低,我习惯按一套固定流程走,基本不会漏。
3.1 准备阶段:账号矩阵与请求基线
动手之前先把"弹药"备齐。至少准备两个同级别的普通账号(A和B),以及一个高权限账号(管理员)。A和B用来测水平越权,管理员用来对照高权限接口,低权限账号用来测垂直越权。
然后做请求基线采集:用A账号把目标系统的核心功能完整走一遍,同时开着代理工具(比如Burp Suite或类似的抓包工具)把所有请求记录下来。这一步的目的是建立"正常请求长什么样"的参照系。重点标记出所有携带资源标识的请求——URL里的id、请求体里的userId/orderId/fileId、以及任何看起来像唯一标识的字段。
我一般会在抓包工具里给这些请求打上颜色标记,方便后面逐个处理。这个准备工作做扎实了,后面的检测就是流水线作业。
3.2 水平越权验证:A的资源让B来碰
核心动作只有一个:把A账号请求里的资源标识,替换成B账号的资源标识,然后用A的凭证重放。如果返回了B的数据,水平越权成立。
具体操作上有几个细节要注意:
- 先确认B的资源标识确实存在且属于B。用B账号登录,找到B的订单号/资料ID,记下来。
- 替换后观察响应。不要只看HTTP状态码,200不代表成功,要看响应体里是不是真的返回了B的数据。有些系统会返回200但内容是空的或者是个错误提示。
- 注意响应差异。如果替换后返回"无权限"或"资源不存在",说明有校验;如果返回了完整数据,就是漏洞。
这里有个容易忽略的点:有些接口的越权不是通过改id触发的,而是通过删掉某个参数触发的。比如正常请求带userId=123,你把它删掉,后端可能就默认查全部或者查成了别人。所以除了"改值",还要试"删参""加参""改参数类型"。
3.3 垂直越权验证:低权限够高权限接口
这个相对直接:把管理员账号能触发的敏感请求(增删改用户、改配置、批量操作)抓下来,换成普通用户的cookie重放。重点看那些只在管理端出现的接口。
一个提效技巧是:不要只盯着页面上的按钮。很多高权限接口在正常页面里根本不会出现,但可能存在于JS文件、接口文档、或者历史版本里。我习惯翻一翻前端打包后的JS,搜一搜admin、delete、update这类关键字,经常能挖出一些"隐藏接口",而这些接口恰恰是最容易漏做权限校验的。
3.4 自动化辅助:把重复劳动交给工具
手工测虽然准,但接口一多就顶不住。我的做法是手工定策略、工具做批量。具体来说,用抓包工具把A账号的所有请求导出,然后用脚本批量做"ID替换+重放",把响应长度、状态码、关键字段做对比,把"疑似越权"的请求筛出来,再人工确认。
下面是一个简化的Python脚本思路,用来说明批量重放的逻辑(实际使用时替换成你自己的请求模板和ID范围):
import requests # A账号的cookie,作为重放凭证 headers = { "Cookie": "session=AAAAAAAA", "Content-Type": "application/json" } # 遍历一批资源ID,看哪些能返回数据 for rid in range(10001, 10050): url = f"https://target.example.com/api/order/detail?id={rid}" resp = requests.get(url, headers=headers, timeout=5) # 简单判断:响应体里包含订单关键字且长度异常,标记出来人工看 if resp.status_code == 200 and "orderNo" in resp.text: print(f"[疑似越权] id={rid}, 长度={len(resp.text)}")这个脚本只是示意,真实场景里要处理签名、时间戳、CSRF token 等。但核心思想就是用固定凭证去遍历资源标识,靠响应特征筛可疑项。筛出来之后,一定要人工用两个真实账号交叉验证,避免误报。
提示:批量遍历资源ID时务必控制频率,并且只在获得明确授权的测试环境或授权范围内进行。未经授权对真实系统做批量遍历,性质就变了。
4. 修复越权:为什么很多"修了"其实没修好
发现漏洞只是第一步,真正考验功力的是修复。我见过太多"改了但没改干净"的情况,这里把常见的修复误区和正确做法讲清楚。
4.1 归属校验必须落在服务端且绑定当前身份
最根本的一条:任何涉及资源读写的操作,服务端都必须用"当前登录身份"去约束查询范围,而不是信任客户端传来的任何身份字段。
具体到代码层面,正确的模式是"从会话中取用户身份,作为查询条件的一部分",而不是"从请求参数里取用户身份"。举个对比:
# 错误做法:直接信任请求里的userId def get_order(request): user_id = request.args.get("userId") # 可被篡改 order_id = request.args.get("orderId") return db.query("select * from orders where id=? and user_id=?", order_id, user_id) # 正确做法:身份从会话取,请求里只传资源标识 def get_order(request): user_id = request.session["user_id"] # 服务端可信来源 order_id = request.args.get("orderId") row = db.query("select * from orders where id=? and user_id=?", order_id, user_id) if not row: return {"code": 403, "msg": "无权访问"} return row关键差异在于身份来源。只要身份是从session/token里解出来的,攻击者就没法通过改参数来冒充别人。这是水平越权修复的核心。
4.2 垂直越权要靠统一的权限拦截,而不是每个接口手写
垂直越权的修复,最忌讳"哪里漏了补哪里"。因为接口成百上千,你不可能保证每个都记得加角色判断。正确做法是在框架层做统一的权限拦截——比如基于注解或中间件,给每个接口声明所需角色,请求进来先过一遍权限矩阵,不满足直接拒绝。
我推荐的做法是维护一张接口-角色权限表,把每个敏感接口需要什么角色明确列出来,然后在网关或框架层统一校验。这样新增接口时,开发者必须显式声明权限,漏声明就默认拒绝(白名单思路),从机制上避免遗漏。
这里有个经验:默认拒绝比默认放行安全得多。如果一个接口没有声明权限要求,系统应该拒绝访问并报错提示开发者补充,而不是放行。很多越权漏洞就是因为"没声明=不校验=放行"这个默认逻辑造成的。
4.3 修复后的回归验证:别只测一个用例
修完之后一定要做回归,而且不能只测"原来那个越权用例"。我一般会验证这几项:
- 原漏洞用例是否已被拦截(A访问B的数据应返回403或空)。
- 正常用例是否仍然可用(A访问A自己的数据要正常)。
- 边界情况:资源不存在、资源属于已注销用户、资源ID格式非法时,返回是否合理且不泄露信息。
- 批量接口:数组里混入他人ID时,是整体拒绝还是部分返回?理想是整体拒绝或过滤掉无权项。
特别提醒一点:错误信息本身也可能泄露信息。如果越权访问时返回"该订单属于用户B",那等于把归属关系告诉攻击者了。修复时统一返回"资源不存在或无权限"这类模糊提示更稳妥。
5. 几个容易翻车的实操细节与个人心得
最后这部分是我踩过坑之后攒下来的经验,都是文档里不会写、但实际干活时特别容易栽跟头的地方。
5.1 "参数删掉反而越权"这种反直觉情况
前面提过一嘴,这里展开说。有些接口的逻辑是"如果传了userId就按userId查,没传就查全部"。这种设计在开发看来是"灵活",在安全上就是灾难——攻击者只要把userId删掉,就能拿到全量数据。我遇到过好几次,测试时改id没成功,随手把参数删了,结果返回了一大片数据。
所以测试时对每个参数都要做增、删、改、重四连:增加一个同名参数、删掉它、改它的值、重复传两次。这四种操作能覆盖大部分参数处理逻辑的缺陷。
5.2 越权不一定返回数据,写操作更危险
很多人测越权只盯着"读",看能不能看到别人数据。但写操作的越权危害往往更大——改别人的密码、删别人的订单、给别人转账。读越权泄露数据,写越权直接造成破坏。
所以测试时要把"修改类"接口单独拎出来重点测:改资料、改密码、删除、转移、支付。这类接口一旦越权,后果是实打实的。验证写越权时,用A的凭证去操作B的资源,然后登录B确认B的数据是否真的被改了——这一步确认不能省,因为有些接口返回成功但实际没生效。
5.3 越权和业务逻辑漏洞经常纠缠在一起
实际项目里,纯粹的越权很少单独出现,它常常和业务逻辑漏洞绑在一起。比如"优惠券领取"接口,既没校验用户是否已领过(业务逻辑),也没校验券的归属(越权),两个问题叠加,就能被刷券。再比如"邀请返利",越权改一下邀请人ID,就能把别人的返利算到自己头上。
我的建议是:测越权的时候顺手把业务规则也过一遍。看这个接口除了"是不是我的",还应该满足哪些业务约束(次数限制、状态流转、金额范围),这些约束缺失往往和越权一样能造成实际损失。
5.4 关于测试边界的一点提醒
越权测试因为涉及遍历和重放,很容易"用力过猛"。我的原则是:只在明确授权的范围内测试,控制请求频率,不碰真实用户数据。在授权测试中,验证到"能返回他人数据"即可停止,不要真的把数据导出来或者做进一步操作。这既是职业操守,也是保护自己。测试环境里可以放开手脚,生产环境必须克制。
另外,报告越权漏洞时,把复现步骤、请求响应截图、影响范围写清楚,比单纯说"存在越权"有价值得多。尤其是影响范围——是单个用户还是全站、是读还是写,这直接决定了漏洞的定级和修复优先级。
这套东西我用了几年,基本能覆盖绝大多数越权场景。真正难的不是技术,而是养成"每个涉及资源的接口都问一句'这该不该是我的'"的习惯。把这个习惯刻进开发和测试的流程里,越权漏洞的生存空间就会小很多。