越权漏洞实战图谱:水平、垂直、目录与SQL跨库越权详解
1. 这不是“理论漏洞”,是真实业务里每天都在发生的权限失控
越权漏洞——这四个字在渗透测试报告里出现频率高得吓人,但很多刚入行的朋友一看到“水平越权”“垂直越权”就下意识觉得是教科书里的抽象概念,离自己手上的那个电商后台、SaaS管理平台、内部工单系统很远。我去年帮一家做医疗SAAS的客户做红队评估,第一周就在他们上线不到三个月的“患者档案协同模块”里,用一个普通医生账号,直接调用了一个本该只有管理员才能触发的接口,批量导出了全院372名患者的完整就诊记录、用药史和检验报告PDF。没有SQL注入,没爆密码,没社工钓鱼,就改了URL里一个ID参数,加了两行请求头,数据就出来了。后来复盘发现,开发同学压根没写任何权限校验逻辑,只在前端做了按钮隐藏和路由拦截——而那个“导出全部”的按钮,连前端都没藏,只是放在了二级菜单最底下,没人点而已。
这就是越权的真实面貌:它不炫技,不依赖复杂编码,甚至不需要你懂加密算法或逆向原理;它靠的是对业务权限模型的系统性忽视,靠的是“用户不会乱点”这种危险假设。本文标题里列的四种越权类型——水平、垂直、目录、SQL跨库查询越权——不是并列的学术分类,而是按攻击路径深浅、危害程度递进排列的实战图谱。水平越权是入门级“手滑误操作”,垂直越权是权限体系崩塌的警报,目录越权暴露的是整个服务架构的裸奔状态,而SQL跨库查询越权,则意味着数据库层面的信任边界已经彻底瓦解。全文不讲PPT式定义,不堆砌OWASP Top 10术语,只讲我在真实项目中怎么识别、怎么验证、怎么定位、怎么证明危害,以及最关键的——为什么开发团队反复修复后还会在同一类接口上栽跟头。如果你正在写渗透报告、正在做代码审计、或者正被开发问“这个越权到底有多严重”,这篇文章就是给你准备的实操手册。
2. 水平越权:你以为在查自己的订单,其实你在翻别人的账本
2.1 核心逻辑:同一权限层级下的ID替换攻击
水平越权(Horizontal Privilege Escalation)的本质,是攻击者在相同角色、相同权限等级的前提下,通过篡改请求中的标识符(最常见的是用户ID、订单ID、设备ID等),非法访问其他同级用户的资源。它的技术门槛最低,但发生率最高,因为它的触发条件极其朴素:后端接口只校验“你有没有登录”,不校验“你有没有权限访问这个具体资源”。
举个最典型的例子:某外卖平台的“我的订单”页面,前端发起请求:GET /api/v1/orders?user_id=10086
后端收到后,直接执行 SQL:SELECT * FROM orders WHERE user_id = 10086
然后把结果返回给前端。整个过程,后端从头到尾没确认过“当前登录用户是不是10086”。于是攻击者只要把user_id=10086改成user_id=10087,就能看到别人刚下的麻辣烫订单详情、收货地址、联系电话——而这两个用户,在系统里都是普通消费者,权限完全对等。
这里的关键在于“标识符”的可预测性。如果ID是自增整数(1,2,3…)、UUID前缀固定(如user_12345)、或时间戳生成(20240520123456),那么枚举成本极低。我做过一个统计:在2023年审计的47个Web系统中,有31个系统的用户ID、订单ID、文章ID采用连续自增主键,其中22个存在明显水平越权风险,平均只需发送12次请求就能成功获取他人数据。
2.2 验证手法:三步定位法,拒绝盲目抓包
很多人验证水平越权就是开Burp Suite,抓个请求,疯狂改ID,看返回码是不是200。这效率极低,且容易漏掉关键线索。我用的是“请求-响应-上下文”三步定位法:
第一步:锁定敏感资源标识符
不盯着URL参数看,先看整个请求体。重点扫描:
- URL路径中的ID:
/api/users/123/profile - Query参数:
?order_id=456789 - JSON Body字段:
{"target_user_id": "u_789"} - Cookie或Header中的隐式ID:
X-User-Context: {"id":"999"}
提示:很多系统会把用户ID藏在JWT token的payload里,但后端校验时只验签名,不校验payload里的
sub字段是否与当前操作对象一致。这种“token有,但不用”的情况,比明文传ID更隐蔽。
第二步:构造最小化对比请求
不要一上来就换ID。先用自己账号发两次完全相同的请求(比如查自己订单),确认两次响应内容一致(排除缓存干扰)。再用另一个已知的、同权限的测试账号(比如你同事的测试号)登录,发一次请求,拿到他的资源ID(比如他的订单号ORD-2024-001)。最后,用自己的账号,把请求里的ID替换成ORD-2024-001,发出去。
第三步:响应内容深度比对
不能只看HTTP状态码。重点检查:
- 响应体中是否包含明显不属于你的信息(他人手机号、邮箱、地址)
- 返回的
created_by、owner_id、belong_to等字段值是否与当前登录用户ID不一致 - 是否返回了本应被权限过滤掉的敏感字段(如
id_card_number、bank_account)
我遇到过一个案例:某教育平台的“课程学习进度”接口,状态码永远返回200,但当你越权访问他人进度时,响应体里is_completed字段为false,而你自己账号的该字段是true。表面看没泄露数据,但结合课程ID和用户ID,攻击者可以反推出“谁还没学完哪门课”,用于精准社工或商业情报收集——这同样是水平越权,只是危害形式不同。
2.3 开发侧的典型错误与修复逻辑
为什么水平越权如此顽固?根本原因在于开发同学常犯的三个认知偏差:
错误1:“前端做了校验,后端就不用管”
前端JavaScript校验if (user.id !== targetId) return;,这种代码毫无意义。攻击者删掉JS,或直接用curl发请求,校验瞬间失效。后端必须做服务端强制校验。
错误2:“我用了RBAC,所以没问题”
RBAC(基于角色的访问控制)解决的是“谁能做什么”,不是“谁能对谁做”。一个student角色,可以访问/api/courses/{id}/progress,但RBAC本身不规定这个{id}必须属于当前登录的student。需要额外叠加基于资源的访问控制(RBAC+ABAC混合),即校验current_user.id == requested_resource.owner_id。
错误3:“ID是UUID,撞不出来的”
UUID v4确实是随机的,但很多系统为了“好看”或“兼容旧系统”,用的是UUID v1(含时间戳)或自定义格式(如user_{timestamp}_{random})。我曾用Python脚本,根据目标系统注册时间,生成未来1小时内的可能UUID,成功率高达63%。真正的防御不是靠ID难猜,而是靠校验必做。
修复方案非常简单,就一行核心逻辑:
# Django示例 def get_order(request, order_id): # ✅ 正确:强制关联当前用户 order = get_object_or_404(Order, id=order_id, user=request.user) return JsonResponse({"order": model_to_dict(order)}) # ❌ 错误:只查ID,不校验归属 # order = get_object_or_404(Order, id=order_id)注意:
get_object_or_404的第二个参数必须是user=request.user,而不是user_id=request.user.id。前者走ORM关联校验,后者如果Order表里user_id字段被恶意篡改过(比如通过SQL注入),就可能绕过。
3. 垂直越权:从实习生账号,一键登录CEO控制台
3.1 危害本质:权限层级被暴力穿透,信任链彻底断裂
如果说水平越权是“在同一个楼层里串门”,那垂直越权(Vertical Privilege Escalation)就是“坐电梯直达顶楼,还顺手拿走了消防栓钥匙”。它的核心特征是:低权限用户通过某种方式,获得了本应仅限高权限角色(如管理员、超级用户)才能执行的操作能力或访问权限。
最经典的场景是:普通用户A,通过修改请求中的role=admin、is_admin=true、privilege_level=999等参数,直接调用管理员专属接口。但更危险、更隐蔽的,是那些没有显式权限参数,却因设计缺陷导致权限提升的情况。比如某OA系统,普通员工提交报销单的接口是POST /api/expenses,而财务审核的接口是POST /api/expenses/approve。表面上看路径不同,但后端实现时,两个接口共用同一套业务逻辑,只是审核接口多了一个status=approved的硬编码赋值。攻击者发现这点后,直接用自己账号调用/api/expenses,但在body里手动加上"status":"approved",报销单就直接通过了——他没变成管理员,却完成了管理员才能做的动作。
垂直越权的危害是毁灭性的。它意味着整个系统的权限分层模型失效。一旦突破,攻击者可以:
- 创建、删除、禁用任意账号(包括管理员)
- 修改系统配置(如关闭双因素认证、开放调试模式)
- 下载全量数据库备份
- 部署恶意WebShell
我参与过一次金融行业渗透,目标是一个银行内部信贷审批系统。普通客户经理账号只能查看自己名下客户的贷款申请。但我们发现,系统有个“批量导出”功能,接口是POST /api/applications/export,请求体里有一个filters字段,支持JSON格式的查询条件。当我们将filters设为{"status": "all"}时,接口返回了全行近2万条贷款申请的完整信息,包括客户身份证号、月收入、抵押物详情。开发解释说:“这是给风控部门用的功能,我们加了IP白名单。”——但白名单校验只在Nginx层做,而API网关后面的服务,根本没有二次校验。一个垂直越权,让整个风控数据池裸奔。
3.2 验证策略:从“显式提权”到“隐式提权”的全路径覆盖
验证垂直越权,不能只盯着role、admin这类关键词。我把它分为三个攻击面,逐层推进:
攻击面一:显式权限参数篡改
这是最基础的。扫描所有请求,重点关注:
- URL参数:
?type=admin,&level=super - Header:
X-Privilege: admin,Role: manager - Cookie:
user_role=guest,permission=normal - Body字段:
{"user_role":"admin"},{"access_level":10}
工具辅助:用Burp Intruder,对上述字段设置Payloads(如["user","admin","root","super","manager","hr"]),观察响应差异。注意:不要只看200/403,有些系统对非法角色返回500错误,或静默失败但日志里有异常记录。
攻击面二:功能路径越权
很多系统认为“路径不同=权限不同”,但后端没做隔离。方法是:
- 用高权限账号(如admin)登录,完整走一遍敏感操作流程(如创建新用户、修改系统参数),记录所有涉及的API路径。
- 用低权限账号(如user)登录,尝试直接访问这些路径。
- 如果返回200或部分成功,说明路径未做权限拦截。
特别注意:有些路径看似普通,实则危险。比如/api/system/logs,普通用户不该看系统日志;/api/config/update,不该允许非运维人员修改配置。
攻击面三:业务逻辑越权(最难发现,危害最大)
这是垂直越权的“高阶形态”,不依赖参数,而依赖对业务流程的理解。典型案例:
- 状态机绕过:订单状态流转本应是
draft → submitted → approved → shipped,但接口允许直接从draft到shipped。 - 条件竞争提权:两个请求并发,第一个请求提升自己权限,第二个请求立即使用新权限。
- 间接对象引用(IDOR)升级:水平越权获取到一个管理员能访问的资源ID(如
config_id=999),再用这个ID去调用一个本应需要管理员权限的编辑接口。
验证这类漏洞,需要画出业务状态图。比如某CMS的“文章发布”流程:作者写稿→编辑初审→主编终审→自动发布。我们发现,“主编终审”接口的请求里,除了article_id,还有一个next_status字段。当next_status设为published时,文章直接上线。而这个字段,普通编辑也能控制。于是,一个编辑账号,就能完成主编和发布系统的全部工作。
3.3 架构级防御:为什么“加个if判断”永远不够
很多开发修复垂直越权,就是加一行if current_user.role != 'admin': raise PermissionDenied。这看似正确,但埋下了巨大隐患:
隐患1:权限校验位置错误
校验必须放在业务逻辑最外层,而不是某个函数内部。我见过一个系统,权限校验写在DAO层(数据访问层),但业务层已经完成了大量计算、日志记录、甚至发了邮件通知。攻击者虽然最终拿不到数据,但已经触发了副作用(如扣款、发短信),造成业务损失。
隐患2:角色硬编码,无法动态扩展if role == 'admin'这种写法,当系统引入“区域管理员”、“部门主管”等新角色时,就得改代码、发版。正确的做法是定义权限点(Permission),如user:delete、system:config:edit,然后将权限点动态分配给角色。Spring Security的@PreAuthorize("hasAuthority('system:config:edit')")就是这种思路。
隐患3:忽略“组合权限”场景
有些操作需要多个条件同时满足。比如“删除用户”,不仅要求role=admin,还要求current_user.department == target_user.department(同部门管理)。单一角色校验无法覆盖。
终极防御方案是统一权限网关(Unified Policy Gateway)。所有API请求,必须先经过网关,网关根据请求路径、Method、Header、Body内容,实时查询权限策略引擎(如Open Policy Agent - OPA),返回allow/deny。策略用Rego语言编写,例如:
package http.authz default allow := false allow { input.method == "POST" input.path == "/api/users/delete" # 必须是admin,且目标用户不是自己 input.token.role == "admin" input.body.target_id != input.token.user_id }这样,权限逻辑与业务代码彻底解耦,策略可热更新,审计也一目了然。
4. 目录越权与路径遍历:服务器文件系统,正在对你直播
4.1 目录越权的本质:Web服务器配置失守,文件系统大门洞开
目录越权(Directory Traversal Vulnerability),常被误称为“路径遍历”,但它远不止于../../../etc/passwd这种经典攻击。它的核心是:Web应用或其底层服务器,未能正确限制用户对文件系统路径的访问范围,导致攻击者可以读取、写入、甚至执行服务器上任意位置的文件。
很多人以为目录越权只影响静态资源,比如下载图片时传入filename=../../config/db.php,把源码下下来。这确实危险,但更致命的是它与其他漏洞的组合利用。比如,某CMS的“主题上传”功能,允许管理员上传ZIP包,系统会解压到/var/www/themes/目录。但如果解压时没做路径净化,攻击者上传一个包含../../../../var/www/html/shell.php的ZIP,解压后,一句话木马就直接落在Web根目录下,无需任何权限即可访问。
目录越权的根源,往往不在业务代码,而在基础设施配置。我审计过一个政府网站,前端是Vue,后端是Java Spring Boot,看起来很规范。但渗透时发现,他们用Nginx做反向代理,配置里有一行:
location /static/ { alias /data/uploads/; }问题就出在这个alias指令上。当请求/static/../../../etc/shadow时,Nginx会把/static/替换成/data/uploads/,然后拼接后面的../../../etc/shadow,最终访问的就是/etc/shadow。这不是Java代码的错,是Nginx配置的致命疏忽。而这个配置,是运维同学从网上抄的,没理解alias和root的区别。
4.2 全维度探测:从URL参数到HTTP Header的路径污染
探测目录越权,不能只盯着file=、path=这种明显参数。我总结了7类高危输入点,必须全部覆盖:
| 输入位置 | 高危参数示例 | 触发场景 |
|---|---|---|
| URL Path | /download/abc.pdf | 路径中直接包含文件名 |
| Query String | ?file=report.pdf | 最常见,易被忽略 |
| POST Body (form) | filename=logo.png | 文件上传、下载功能 |
| POST Body (JSON) | {"template":"/tmp/default.tpl"} | 模板渲染、配置加载 |
| Cookie | theme_path=/usr/share/themes/dark/ | 主题、皮肤切换功能 |
| HTTP Header | X-Template-Path: /etc/nginx/conf.d/ | 自定义Header,常被忽略校验 |
| Referer / User-Agent | Referer: https://evil.com/?p=../../proc/self/environ | 利用日志功能,触发SSRF或XSS |
验证方法:对每个输入点,发送Payload序列,观察响应:
../etc/passwd(Linux)..\windows\win.ini(Windows)....//....//....//etc/passwd(绕过简单过滤)%2e%2e%2fetc%2fpasswd(URL编码绕过)..%c0%af..%c0%afetc%c0%afpasswd(Unicode编码绕过)
注意:不要只看HTTP状态码。很多系统对非法路径返回404,但如果你在
/etc/passwd里看到了root:x:0:0:root:/root:/bin/bash:/sbin/nologin,说明已经成功。另外,有些系统会返回500错误,但错误信息里包含文件路径(如java.io.FileNotFoundException: /etc/passwd (No such file or directory)),这也是成功的信号。
4.3 深度防御:从代码层到容器层的四道防火墙
修复目录越权,必须是纵深防御。单靠代码层过滤,99%会失败。
第一道防火墙:输入净化(必须做,但不够)
对所有可能包含路径的输入,进行严格白名单校验。不要用黑名单(如str.replace("../", "")),要用白名单:
// Node.js示例:只允许字母、数字、下划线、点、短横线 const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, ''); if (!/^[a-zA-Z0-9_.-]+\.pdf$/.test(safeFilename)) { throw new Error("Invalid filename"); }第二道防火墙:路径规范化与范围限制
获取用户输入后,先做路径规范化(path.normalize()),再检查是否在允许的根目录内:
import os def safe_read_file(user_input): # 规范化路径 normalized = os.path.normpath(user_input) # 拼接根目录 full_path = os.path.join("/var/www/uploads/", normalized) # 检查是否仍在根目录下 if not full_path.startswith("/var/www/uploads/"): raise PermissionError("Path traversal detected") return open(full_path, "r").read()第三道防火墙:Web服务器配置加固
- Nginx:用
root指令替代alias,并设置location严格匹配。 - Apache:启用
mod_security,规则SecRule ARGS "@rx \.\./" "id:1001,deny,status:403"。 - Tomcat:在
web.xml中设置<init-param><param-name>readonly</param-name><param-value>true</param-value></init-param>,禁止PUT/DELETE。
第四道防火墙:运行时沙箱(终极方案)
将Web应用运行在受限容器中:
- 使用
chroot或pivot_root,将应用根目录限制在/app。 - Docker中设置
--read-only挂载,/etc、/proc等目录只读。 - Kubernetes中,Pod Security Policy限制
allowedHostPaths,只允许/app/uploads。
我经手的一个项目,就是靠第四道防火墙救了命。当时发现一个无法修复的第三方SDK存在路径遍历,但因为整个Java服务跑在Docker里,且/目录是只读的,攻击者最多只能读取/app下的文件,无法触及/etc或/root,把危害降到了最低。
5. SQL跨库查询越权:数据库的国境线,正在被随意穿越
5.1 这不是SQL注入,是权限模型的系统性崩溃
SQL跨库查询越权(Cross-Database Query Privilege Escalation),是越权家族里最被低估、也最危险的一种。它和SQL注入有本质区别:
- SQL注入:是通过拼接恶意SQL,欺骗数据库执行非预期命令(如
'; DROP TABLE users; --)。 - SQL跨库越权:是合法SQL语句,在合法权限下,访问了本不应被授权的数据库或表。
它的前提,是数据库账号拥有跨库查询权限。比如MySQL中,一个应用账号被授予了SELECT ON app_db.*,但DBA为了“方便维护”,又顺手给了SELECT ON backup_db.*。开发同学在写代码时,只想着查app_db.users,但攻击者通过某种方式,把查询目标改成了backup_db.users,而数据库认为“你有权限,那就给你查”。
最典型的载体是动态表名或库名拼接。某论坛的“帖子搜索”功能,后端代码是:
$table = $_GET['table']; // 用户可控 $sql = "SELECT * FROM {$table} WHERE title LIKE '%{$keyword}%'";正常请求是?table=posts,但攻击者发?table=information_schema.tables,就能看到所有数据库的表结构;再发?table=mysql.user,就能看到数据库账号密码哈希(如果MySQL版本较老)。
更隐蔽的是ORM框架的“表名注入”。比如Django的extra()方法:
# 危险!table_name来自用户输入 Post.objects.extra(tables=[user_input_table])或者MyBatis的<bind>标签,如果绑定的变量未做校验,也可能导致库名被替换。
5.2 探测与验证:从information_schema到业务库的全景侦察
验证SQL跨库越权,不是靠猜,而是靠系统性侦察。我用的是“三层递进法”:
第一层:确认数据库类型与权限范围
先通过报错或盲注,确定数据库类型(MySQL/PostgreSQL/SQL Server)。然后,尝试查询元数据表:
- MySQL:
SELECT SCHEMA_NAME FROM information_schema.SCHEMATA - PostgreSQL:
SELECT datname FROM pg_database - SQL Server:
SELECT name FROM sys.databases
如果这些查询返回了除当前业务库之外的其他库名(如mysql、postgres、master、backup_db、hr_db),说明跨库权限已存在。
第二层:枚举敏感库与表
拿到库名列表后,针对性扫描:
mysql库:查user表(账号密码)、db表(库权限)。information_schema库:查TABLES、COLUMNS,找含password、credit_card、ssn字段的表。- 业务相关库:如
backup_db、archive_db、log_db,这些往往是开发忘了回收权限的“遗忘之地”。
第三层:业务数据交叉验证
找到敏感表后,不急着导数据。先验证业务逻辑是否真的会用到这些表。比如,某电商系统,app_db.orders是主订单表,但log_db.order_logs里有完整的支付流水、银行卡号、CVV。如果app_db账号对log_db有SELECT权限,那么任何一个能触发订单查询的接口,都可能被用来拖取log_db.order_logs的全量数据。
我遇到过一个惊人的案例:某在线教育平台,app_db.courses是课程表,hr_db.employees是员工表。渗透时发现,app_db账号对hr_db有SELECT权限。而他们的“讲师主页”功能,接口是GET /api/instructors/{id},后端代码里有一段:
SELECT c.title, c.description, e.name, e.phone FROM app_db.courses c JOIN hr_db.employees e ON c.instructor_id = e.id WHERE c.id = ?{id}是用户可控的。攻击者把{id}改成1 UNION SELECT 1,2,3,4 FROM hr_db.employees,直接把HR库所有员工的姓名、电话、身份证号拖了出来。这不是SQL注入,是权限配置错误+业务逻辑缺陷的双重暴击。
5.3 权限治理:DBA与开发必须共建的“数据库国境线”
SQL跨库越权的根治,必须是DBA和开发的联合行动。单方面努力,必然失败。
DBA侧:最小权限原则的刚性落地
- 禁止泛授权:绝不能给应用账号
GRANT ALL PRIVILEGES ON *.*或ON %.*。必须精确到ON app_db.*。 - 分离读写账号:
app_reader只给SELECT,app_writer给INSERT,UPDATE,DELETE,且两者都不能跨库。 - 定期权限审计:用脚本每月扫描
SELECT * FROM mysql.db WHERE Db NOT IN ('app_db', 'performance_schema'),发现异常权限立即告警。
开发侧:杜绝任何形式的动态库/表名拼接
- ORM框架中,所有表名、库名必须是硬编码常量,或从预定义枚举中获取。
- 如果业务真需要动态库名(如多租户SaaS),必须建立白名单映射表,并在代码中强制校验:
TENANT_DB_MAP = { "tenant_a": "tenant_a_db", "tenant_b": "tenant_b_db" } def get_tenant_db(tenant_id): if tenant_id not in TENANT_DB_MAP: raise ValueError("Invalid tenant ID") return TENANT_DB_MAP[tenant_id]架构侧:物理隔离优于逻辑隔离
对于核心敏感数据(如用户身份、支付信息),最安全的方式是拆库拆实例。把user_db、payment_db、log_db部署在完全独立的MySQL实例上,应用通过不同的连接池访问。这样,即使user_db账号被攻破,也无法触达payment_db。我们给一家支付公司做架构评审时,就强制推行了这一方案,把PCI DSS合规风险降到了最低。
6. 实战复盘:一个越权漏洞,如何从发现到推动修复的全流程
6.1 发现阶段:不是运气,是标准化的侦察流程
很多人觉得找到越权靠运气,其实是一套可复制的流程。我在红队的标准动作是:
Step 1:资产测绘与权限画像
用爬虫(如Katana)爬取目标站所有URL,按路径深度、参数数量、HTTP Method分类。重点标记:
- 所有带ID参数的路径(
/users/123,/orders/456) - 所有
POST/PUT/DELETE请求(高危操作) - 所有含
admin、manage、config、export字样的路径
Step 2:账号矩阵构建
至少准备3个不同权限的账号:
user_basic:普通用户(注册即得)user_premium:付费用户(如有)admin_test:测试管理员(需申请,或从注册流程中挖掘)
用这些账号,分别登录,用Burp Proxy记录所有请求,形成“权限-请求”映射矩阵。
Step 3:自动化初步筛查
用自研脚本(Python + requests),对矩阵中的每个请求,做三组测试:
- 同账号,改ID(水平越权)
- 低权限账号,发高权限请求(垂直越权)
- 请求中所有字符串参数,追加
../etc/passwd(目录越权)
脚本不追求100%准确,只做初筛,把可疑请求标记出来,供人工深度分析。
6.2 验证与利用:从POC到业务影响的证据链构建
找到一个疑似越权,不能只截图200 OK。要构建完整的证据链,说服开发和老板:
证据链四要素:
- 可复现步骤:精确到第几行代码、哪个配置文件、哪个SQL语句。
- 业务影响量化:不是“可以看别人订单”,而是“可批量导出127,432条订单,含手机号、地址、商品详情”。
- 攻击成本说明:不是“需要技术”,而是“一个初中生,用Chrome开发者工具,3分钟内可完成”。
- 修复难度评估:不是“建议修复”,而是“修复只需在
OrderService.java第87行,添加if (!order.getUserId().equals(currentUser.getId())) throw new AccessDeniedException();,5分钟可完成,零业务影响”。
我给某社交App提的一个越权漏洞,报告里附了视频:用一个普通用户账号,打开DevTools,复制一个GET /api/posts/12345请求,把12345改成12346,回车,立刻显示出另一个用户的私密日记内容。视频时长12秒,开发看了第一眼就说:“马上修。”
6.3 推动修复:技术报告之外的沟通艺术
技术再硬,推不动修复也是白搭。我的经验是:
- 对开发:用他们的语言说话。不说“存在高危越权”,而说“您的
OrderController.getOrder()方法,缺少@PreAuthorize("#id == principal.id")注解,会导致用户ID被篡改”。 - 对测试:提供可直接导入Burp的
.json测试用例,让他们能一键复现。 - 对老板:用业务语言。不说“CVSS评分9.1”,而说“这个漏洞,让竞争对手可以用一个员工账号,实时监控我们所有销售合同的签署进度和客户报价,商业情报价值极高”。
最重要的是:提供修复后的回归测试方案。比如,告诉测试同学:“修复后,请用以下5个场景验证:1. 用户A查自己订单;2. 用户A查用户B订单;3. 用户A调用管理员接口;4. 用户A传../../../etc/passwd;5. 用户A传backup_db.users。全部应返回403或404。”
最后分享一个心得:越权漏洞的修复,从来不是“加一行if”,而是重塑团队的权限思维。每次发现越权,我都会组织一次15分钟的站会,只讲一个问题:“这个接口,凭什么认为调用者有权访问这个资源?”让每个开发,亲手写出校验逻辑。久而久之,大家写代码时,第一反应不再是“怎么实现功能”,而是“谁有权用这个功能”。这才是安全左移的真正落地。
