越权漏洞详解:水平越权与垂直越权的原理、测试与修复

📅 发布时间:2026/10/19 2:25:52
越权漏洞详解:水平越权与垂直越权的原理、测试与修复
平时做安全测试或者代码审计跟人聊起漏洞分类最容易一句话带过又最容易被低估的就是“越权”。很多人一听“越权”下意识觉得不就是权限校验没过嘛改个ID试试就出来了没什么技术含量。但真到实战和修复阶段垂直越权和水平越权这两类问题常常把开发折腾得够呛也让安全工程师在评审会上反复拉扯。这篇就把这两类失效的访问控制掰开揉碎从原理到测试手法再到修复落地方案完整梳理一遍。先说个真实场景。某个内部管理后台普通用户登录后能访问个人资料页某天有人手痒把URL里的/user/profile/101改成/user/profile/102结果看到了另一个人的手机号和收货地址。这就是典型的水平越权两个权限等级相同的用户A能访问B的数据。再换个场景后台有个“导出全部订单”的按钮本来只有管理员能用结果普通用户把请求里的角色字段从user改成admin或者直接调用了那个管理接口居然也返回了全量数据这就是垂直越权。两种越权表现不一样但根子上都是同一件事服务端没有正确地验证“当前用户到底有没有权限做这件事”。这篇文章想聊透的就是这两类问题。适合正在做Web开发、写接口的人看也适合刚接触安全测试、想搞明白越权漏洞到底怎么挖怎么修的同行参考。会讲到访问控制的基本模型、两类越权的本质和案例、测试时的思路和工具还有最后修复时的关键落地点。1. 先搞清楚访问控制到底控的是什么很多人把“认证”和“授权”混为一谈这是理解越权问题最大的障碍。认证解决的是“你是谁”的问题——登录、会话、Token都是这一层的事授权解决的是“你能干什么”的问题——某个请求进来系统判断你是否有权利执行这个操作、访问这份数据。越权漏洞基本都出在授权这一层而不是认证这层因为系统已经知道请求者是谁了只是没校验清楚他够不够格。1.1 访问控制的两个基本概念先对齐视角我习惯把访问控制分成两个动作识别主体和验证权限。识别主体就是从请求里取出当前用户是谁可能是Session里的用户ID可能是JWT里的sub字段也可能是一个自定义请求头。验证权限则是拿着这个用户ID去问权限系统他能不能访问这个资源、能不能执行这个操作。问题往往出现在第二步形同虚设。很多接口写完就上线了开发默认“前端没显示这个按钮用户就不知道这个功能”于是后端不校验。这种思路在内部工具里还能侥幸一阵子一旦服务暴露在公网或者被内网用户盯上立刻就会被薅穿。攻击者根本不需要什么高级技巧抓个包改一下参数就能横着走。从设计角度讲访问控制可以建模成一个三元组主体、客体、动作。主体是发起请求的用户客体是你想保护的资源比如订单记录、文件、配置项动作是读、写、删、执行这类操作。越权漏洞的本质就是系统对某个三元组的授权判断出了问题该拒绝的放行了或者该限制范围的没限制住。1.2 权限判断的三个核心要素主体、客体和动作用一个订单查询接口来举例。请求长这样GET /api/order/detail?orderId10086服务端要做的判断有三层。第一层orderId10086这个订单存不存在第二层这个订单的所有者是不是当前登录用户第三层当前用户有没有查询订单详情的角色权限。三层都过了才该返回数据。真实系统里经常只做了第一层甚至第一层都没做好数据库查不到就返回空查到了就直接把整条订单数据吐出来。这就是典型的水平越权——客体是订单动作是查询主体是任意登录用户系统没有校验主体与客体之间的归属关系。再看另一个典型接口POST /api/admin/user/delete Content-Type: application/json { userId: 55 }如果这个接口存在于生产环境却没有对调用者的角色做校验那任何登录用户都能删人这是垂直越权。垂直越权关注的是“角色等级”水平越权关注的是“数据归属”两者互相独立但又经常同时出现在同一个系统里。1.3 从RBAC看常见的权限模型目前业务系统用得最多的是RBAC基于角色的访问控制。核心思想是把权限授予角色把角色授予用户避免每个用户单独配权限的痛苦。比如“订单管理员”这个角色拥有订单查询、导出的权限普通用户只有查看自己订单的权限。逻辑上RBAC很清晰落地时却很容易跑偏。最常见的问题是把角色信息直接放在前端可控的位置比如Session里存一个roleadmin或者JWT payload里塞一个isAdmintrue服务端拿到后完全不验签、不查库就信了。这种设计等于把权限系统的钥匙挂在大门口谁路过都能摘走。基于此看后面两类漏洞会容易很多。垂直越权是RBAC模型的“角色校验缺失”水平越权则是“客体归属校验缺失”。修复的手段、测试的思路都从这两个缺失点出发。2. 垂直越权普通用户如何拿到管理员权限垂直越权也叫权限提升。攻击者从低权限角色通过各种手段访问到高权限角色的功能。它不涉及横向的数据串门而是纵向的等级跨越。这类漏洞一旦存在轻则普通用户调用几个管理接口重则直接控制整个后台。2.1 一个案例看明白垂直越权以前梳理过一个模拟项目X是一个内容管理后台。核心功能分两块普通编辑能写文章、改自己的草稿管理员能审核文章、管理所有用户。权限设计用的是中间件拦截// 伪代码 app.use(/api/admin, authMiddleware);这个authMiddleware只校验了用户是否登录完全没有校验角色。也就是说只要登录了任何人都可以访问以/api/admin开头的所有接口包括“删除用户”“修改全局配置”“审核通过”这些高危操作。测试时直接用普通编辑账号发起POST /api/admin/article/audit { articleId: 123, action: approve }返回200审核通过。普通编辑瞬间拥有管理员权限这就是教科书级的垂直越权。这类问题的高发原因一是中间件只做了“登录校验”没做“角色校验”二是开发觉得接口路径带admin就是安全了忽略了服务端才是唯一的信任边界。2.2 垂直越权的高发场景从Web端到API层垂直越权常出现在三类位置。第一类是管理后台的Web接口尤其是那些用传统Cookie Session的老系统中间件把登录和权限混在一起写能跑就行哪还管校验逻辑是否完备。第二类是前后端分离后的API网关网关做了认证但下游服务各自为政有的校验角色有的不校验整体权限配置支离破碎。第三类是内部系统之间的服务调用部分企业把内部API当作“可信网络”服务间调用不携带身份信息一旦某个边缘服务被攻破攻击者就能以服务身份调用所有高权限接口。我印象很深的一个案例某公司的一个报表系统前端只给管理员显示“导出报表”按钮普通员工看不到。但按钮只是被隐藏了对应的后端接口照样监听所有请求。有好奇的员工直接拼了个URL成功下载了全公司的薪资报表。事后排查后端路由连个角色判断都没写按钮隐藏纯粹是前端的事。注意所有人都在讲“前端不能作为安全边界”但落实到代码里前端隐藏菜单、隐藏按钮的情况依然大面积存在。建议所有涉及敏感操作的接口不管前端显不显示入口后端一律默认拒绝按最小权限原则显式放行。2.3 为什么这么容易犯这类错误这背后有几个深层原因值得聊一聊。第一权限校验代码缺乏统一出口。很多项目里角色校验散落在各个Controller里每个接口自己写一套if (user.role ! admin)大概率会在新增接口时漏掉。一旦团队人员流动、代码交接不清新人照着旧接口复制粘贴很容易只复制业务逻辑没复制权限判断。第二接口设计没有清晰的权限边界。/api/admin/xxx和/api/user/xxx混在同一套路由里或者干脆不做路径区分全靠代码内部逻辑判断。时间一长谁也说不清哪些接口是管理端专用的。第三对“内部系统”过度信任。访问控制失效往往不是因为技术做不到而是根本没想做。很多开发认为公司内网是安全的这个前提本身就站不住脚。内网用户同样可能是攻击者或者某个普通账号被钓鱼之后攻击者拿到的是合法会话直接就变成了“内部人”。3. 水平越权平行用户之间的数据“串门”水平越权比垂直越权更隐蔽危害也更直接。两个用户权限等级相同都能访问自己的数据差别只在于系统没有验证“这份数据是不是你的”。这种漏洞在社交App、电商平台、医疗系统里非常致命出了事就是用户隐私泄露严重的还会触碰合规红线。3.1 水平越权的本质和IDOR的关系水平越权通常和IDORInsecure Direct Object Reference不安全的直接对象引用强相关。所谓直接对象引用就是请求里直接携带了资源标识符比如订单号、用户ID、文件编号服务端拿到这个标识符就去数据库查询返回结果前不检查归属。举个例子某平台有一个“获取收货地址”的接口GET /api/address/get?addressId2088如果服务端只校验地址ID是否存在不校验当前用户和这个地址之间的绑定关系那我只要把addressId改成别人的ID就能拿到别人的收货地址。这就是最典型的水平越权攻击者不需要任何特权唯一需要的就是遍历ID。这类漏洞之所以叫“水平”是因为攻击者和受害者在权限体系里平级攻击者只是利用数据归属校验的缺失完成了越权访问。3.2 真实系统里的水平越权重灾区水平越权高发的功能点有几类这里列一下测过的朋友应该会有共鸣。第一类是“根据ID查询详情”的通用接口。订单详情、用户资料、发票信息、工单记录凡是存在单条数据详情页的系统基本都有这个接口。有些系统生成ID用的是自增整数遍历起来就是分分钟的事有些用UUID虽然不好猜但如果ID出现在某个页面的源码或历史链接里一样能被打到。第二类是批量操作接口。比如“批量下载附件”“批量导出数据”请求体里是一个ID数组服务端遍历数组去数据库取数据没有逐个校验归属。攻击者只要把数组里混入几个别人的ID就能把别人的数据打包带走。这类批量接口危害更大因为一次性泄露的数据量远远大于单条查询。第三类是文件下载和图片访问。很多时候请求参数是文件路径或者文件ID服务端拿到路径直接往磁盘上读文件完全没有和当前用户关联。曾经遇到过附件系统把文件按数字ID命名路径就是/files/upload/10086.pdf测试时把ID减一减上一份文档就下来了连权限都不用绕过。第四类是状态变更接口。比如取消订单、修改密码、转移工单这类接口往往是“改”操作水平越权的后果比读取更严重因为还能影响他人数据。收到一个“请帮我取消订单”的客服工单时一定要想到如果取消订单接口不校验订单归属任何一个登录用户都能用订单ID取消掉别人的订单。3.3 水平越权的变体从“改ID”到“找参数”除了最直观的“改ID”水平越权还有一些变体值得关注。一种是间接对象引用。请求里不直接携带ID而是携带一个“编号”或者“短码”比如一个邀请链接里的邀请码服务端把邀请码换算成用户ID后去查询相关数据。如果换算逻辑没有校验归属攻击者使用别人的邀请码就能看到对应的私有信息。另一种是通过父子关系绕过。某个接口看似校验了第一层归属但忽略了第二层。比如一个“查询订单里的商品”接口GET /api/order/items?orderId10086服务端校验了orderId属于当前用户返回了该订单下的所有商品。商品里有一个字段叫sellerId是商家的ID。攻击者随后用这个sellerId去访问“商家店铺详情”“商家结算记录”等接口而这类接口可能只校验“是否登录”不校验当前用户是否真的是这个商家。这就从订单数据的水平越权扩展到了商家数据的水平越权。测试时不能只看单个接口要顺着参数链往下追看看返回数据里有没有可以作为下一次请求凭证的字段。还有一种是通过批量接口绕过单条限制。单条查询接口已经做了归属校验但批量接口没有或者批量接口的校验逻辑可以被特定参数绕过比如传一个空数组、传一个超大的limit、传一个0。这类问题在“数据导出”功能里尤其常见值得重点关注。4. 实战测试方法如何高效挖掘越权漏洞聊完两类漏洞的原理进入实操环节。越权漏洞的测试思路其实比较固定核心就是两问当前用户能访问哪些不应该访问的功能当前用户能访问哪些不属于自己的数据围绕这两问展开效率会高很多。4.1 挖掘前的信息准备先摸清接口和数据动手测试之前先花时间收集接口信息和理解业务流程。别一上来就扫描没有业务上下文扫描出来的接口也不知道哪个能打。第一步浏览一遍应用的所有功能点把能点的按钮都点一遍把产生的网络请求都记下来。特别关注带有ID、编号、文件名的请求参数。第二步把接口按功能分类查询类、修改类、删除类、导出类、管理类区分哪些是个人数据操作哪些是管理端功能。第三步注册两个不同权限的测试账号至少一个普通账号、一个管理员账号如果有条件再注册一个不同的普通账号作为水平越权的受害者账号。实操心得建议准备两个独立的浏览器环境或者使用支持多Profile的抓包工具保证两个账号的会话相互隔离。切换账号时记得清理Cookie不然很容易测出身份混乱的假阳性。信息收集阶段做到位之后就能针对性地发起测试了。4.2 手工测试的几条实用主线主线一垂直越权排查。用普通账号登录把所有以/admin、/manage、/backend开头的接口全部列出来逐个用普通账号的会话重放。只要有一个返回了非403非401的响应就存在越权可能。再进一步找那些“普通用户功能”和“管理员功能”共用一套路由的接口比如文章审核、订单退款切换到普通账号的会话去请求看能否触发。主线二水平越权排查。使用A账号登录正常请求自己的数据把请求中的ID记录下来。换B账号登录把A的ID代入B的请求对比响应内容。如果B能拿到A的数据基本可以确认水平越权。测试重点集中在“根据ID查询详情”“根据编号下载文件”“根据ID修改状态”这三类接口。主线三抓取返回数据里的“下一次请求凭证”。每个接口的返回JSON都要仔细看有的字段虽然当前页面不展示但可能包含ID、编号、Token。把这些字段收集起来作为后续请求的参数。很多绕过并不在接口本身而在于返回数据里藏了不该出现的敏感字段。主线四检查HTTP方法。同样的URLGET和POST的权限有时不一样。有的接口GET请求校验了角色POST请求却什么都不校验。开发写权限判断时可能只在某个方法上做了处理其他方法直接放过了。4.3 工具辅助测试的思路手工测试能覆盖大多数场景但接口一多就会累。可以用工具辅助做一部分自动化工作。简单场景下直接用抓包工具或命令行构造批量请求就够了。比如用Python写个小脚本测试一批IDimport requests s requests.Session() s.cookies.set(sessionid, your_test_session) for order_id in range(1001, 1021): resp s.get(fhttps://example.com/api/order/detail?orderId{order_id}) if resp.status_code 200: print(f[] Found accessible order: {order_id})这个脚本会逐个尝试范围内的订单ID打印出能够成功访问的条目。需要注意压测式地遍历大量ID可能触发风控或产生大量日志测试前先确认目标环境允许且在测试环境操作不要直接对着生产库跑。更高阶的做法是借助API测试工具先抓取正常请求生成模板再利用数据驱动的方式批量测试。当然工具只是辅助核心还是测试思路。没有对业务的理解工具扫出来的告警也很难判断是真漏洞还是业务设计的正常逻辑。5. 修复方案从代码逻辑到架构设计修复越权漏洞不是一个接口一个接口地打补丁那样永远修不完。正确思路是建立一套完整的、统一的访问控制机制让所有接口默认安全。5.1 修复的核心一条后端是唯一的信任边界这句话说多少次都不为过所有权限校验必须在服务端完成前端展示、按钮隐藏、菜单过滤都只是体验优化不是安全措施。具体落地时最少要做到以下几点。第一登录后建立服务端可控的权限上下文。不要在请求参数里传角色不要信任前端传来的任何权限标识。从Session或数据库中读取当前用户的完整角色和权限集合这是权限判断的唯一数据源。第二所有涉及敏感操作的接口必须显式声明所需权限。可以做成装饰器或注解也可以做成配置化的权限清单。比如app.route(/api/admin/user/delete, methods[POST]) require_role(admin) def delete_user(): ...require_role这个装饰器统一处理角色校验业务代码只关注业务逻辑。第三数据级权限判断必须伴随每一次数据访问。角色校验只是垂直层面的门槛水平层面还得确认当前用户对某个特定资源是否具备操作资格。比如查询订单详情时在SQL层面就加上AND user_id ?而不是查出结果再在内存里比对。SQL层的约束最不容易漏也最不容易被绕过。注意在内存中比对用户ID和资源归属虽然也能实现校验但存在两个风险。一是容易漏新接口复制粘贴时可能忘了那次if判断二是逻辑复杂后难以维护。最好在数据查询层直接按归属过滤从源头拿不到不该拿的数据。5.2 统一鉴权模型的落地统一的鉴权模型是把散落在各个接口里的权限判断逻辑集中管理。我见过几个比较有效的落地方式分享出来供参考。第一种是基于RBAC做统一中间件。系统把所有接口和维护的角色维护成一张路由权限表请求进来时中间件根据当前用户角色查这张表有权限就放行没有就拒绝。这种方式的关键在于路由权限表的完善程度新接口上线时要记得注册进表里否则默认拒绝。第二种是基于ACL做资源级校验。对于数据归属要求特别严格的系统可以维护“用户-资源”的访问控制列表。每次请求资源前先查ACL里是否有当前用户对该资源的访问授权。比如文档类产品用户A创建了文档1ACL里记录A对文档1有读写权限用户B请求文档1时ACL里没有记录直接拒绝。这种方式数据模型清晰但维护成本较高适合对数据隐私要求极高的业务。第三种是使用现有的权限框架。很多成熟框架提供完整的权限注解、表达式、过滤器机制比如Spring Security、Django Guardian等。直接用框架的能力比自己写一套权限工具更稳妥。安全领域最怕“重复造轮子”成熟框架踩过的坑比你多得多。5.3 纵深防御审计和日志不能省即使加了权限校验也不能保证万无一失。纵深防御的最后一环是完整的审计日志和监控告警。日志至少要记录以下信息谁用户ID在什么时间调用了哪个接口传入了什么参数返回了什么结果。这些日志平时看似无用一旦发生安全事故就是排查的第一手资料。如果日志只记录“接口被调用”没记录具体用户和参数事后根本没法追溯。监控告警方面关注一些异常特征单个用户短时间内大量调用某个详情接口单个用户依次访问递增的ID序列普通用户频繁访问管理端接口。这些特征都可以做成规则触发后通知安全人员介入。很多越权攻击的第一步就是批量探测如果能在探测阶段就发现并阻断后面的事就不会发生。6. 常见误区与排查实录最后一部分把经常遇到的误区和问题整理一下这些内容大部分来自实际项目里的踩坑记录遇到类似情况可以直接对照排查。6.1 高频问题速查表问题现象可能原因排查方向前端隐藏按钮但接口可访问后端缺少角色校验抓取管理端请求用普通账号重放修改请求中的ID后能访问他人数据缺少数据归属校验检查详情、导出、下载接口的参数处理逻辑同一接口GET有权限控制POST没有权限判断只覆盖了部分HTTP方法遍历同一URL的GET/POST/PUT/DELETE方法批量导出接口可导出他人数据批量接口未逐条校验归属分析导出请求的数据筛选参数和ID数组接口有权限校验但仍越权成功校验顺序或上下文获取方式有误检查权限判断是取请求参数还是服务端Session越权返回200但数据为空可能已拦截或数据本就不存在切换账号测试观察日志中的校验逻辑分支这个表里的每一个现象都是测试或修复过程中真实遇到的。遇到类似问题时先别急着打补丁对照表格分析一下根因往往能更快定位到真正的问题代码。6.2 一次典型排查过程的还原简单还原一次完整的排查过程体现一下思路。某公司运营反馈使用普通员工账号登录后台后能通过修改URL访问到部门的薪资汇总页面。安全组介入后第一件事是模拟复现用员工账号登录访问带参数的管理页面URL确认能返回数据。第二步是定位代码拿到接口名称后在代码库中搜索对应的视图函数发现该视图只继承了一个“登录校验”的基类没有继承“管理员校验”的基类审查者一眼就找到了缺失的权限判断。第三步是确认影响范围用同样的方式遍历同目录下的其他视图发现还有三个接口存在同样问题。第四步是修复在基类增加统一的角色校验同时用脚本批量审计所有继承该基类的视图函数确保没有遗漏。整个过程不复杂但很能说明问题。越权漏洞的修复往往不是改一处而是改一类。如果只修了一个接口就宣布完成同类问题大概率还会在别的地方冒出来。再分享一个经验在修复完成后建议重新跑一遍整个测试流程用低权限账号逐一访问所有受影响的接口确认返回403或登录跳转。千万别只在代码层面看着没问题就收工真实环境里的URL路径、反向代理配置都可能影响最终结果。另外从研发流程上可以做个小改进。接口设计文档里增加“权限要求”一栏每个接口上线前都要写明允许哪些角色访问、能否访问他人数据。这个字段虽然简单但能逼着开发在写接口时就想清楚权限问题。很多越权漏洞的根源是接口上线时压根没人想过权限这件事。测试越权和修复越权没有什么玄学。核心就是对权限模型的“角色维度”和“数据维度”两条线做完整的梳理和校验。角色维度解决垂直越权数据维度解决水平越权两者都做到位了这类问题自然就堵住了。希望这篇能帮你少踩几个坑。