做Web安全这些年,我见过太多团队把OWASP TOP10挂在嘴边,可真要较真问一句“Broken Access Control到底怎么复现、怎么修、怎么防止下次再犯”,会议室里往往就安静了。正巧最近在重翻这个系列的笔记,聊到榜单上这个常年霸榜的老熟人——2017版里它排在A5,到了2021版直接冲到A01,中文名叫“访问控制失效”。名字听着挺技术,说白了就是:该管住的地方没管住,不该让人访问的数据被翻出来了。这篇我打算把它的原理、高频漏洞形态、测试套路、加固代码一次讲透,不管你是开发、测试、安全工程师,还是被“改几行代码就上线”催着走的技术负责人,都能照着落地。1. 榜单上的“常驻嘉宾”:A5-Broken Access Control到底有多严重1.1 从A5到A01:排名变化背后的信号OWASP TOP10是Web安全领域公认的“风险排行榜”,每三四年更新一次,依据来自全球数百家机构提交的真实漏洞数据。很多人只记了个榜单名称,没细看排名变化背后的含义。2017版里Broken Access Control排在第5位,到了2021版直接升到第1位,这不是什么数据抖动,而是统计口径里这类漏洞的样本量实在太大了,大到把榜单头部整个占住。为什么样本量会这么大?因为“访问控制失效”从来不是一个单一漏洞,而是一个漏洞家族。官方在统计时把它对应的CWE都挂了进去,包括CWE-862(缺失授权)、CWE-863(授权不正确)、CWE-639(通过用户可控键绕过授权)、CWE-352(跨站请求伪造)等一堆子类型。换句话说,你只要在业务里出现“用户能访问本不该访问的数据”这类现象,都可能被归到这个家族里。这也解释了为什么这期系列我觉得值得单独拿出来写——它不是某个冷门技术点,而是每一家企业都有可能踩中的普遍性问题。1.2 为什么越权问题这么普遍我复盘过不少安全评估项目,越权类漏洞几乎霸占了中高危漏洞排查单的前排。它这么普遍,绕不开三个根因。第一,业务复杂。一个系统里光角色就有普通用户、管理员、审计员、客服,再加上多租户、多组织、多项目的数据范围,权限模型稍微设计得不严谨,就会出现“管住了角色、没管住数据范围”的漏洞。第二,开发习惯。很多接口开发时的惯性思维是“能拿到数据就行”,只在入口处校验了“是否登录”,忘了校验“是否有权限看这条数据”。更常见的是,后端默认信任前端传来的用户身份字段,比如把user_id放在请求体里,后端直接拿它当当前用户,这等于把门锁钥匙挂在门框上。第三,安全测试有盲区。传统的自动化扫描器很难发现越权漏洞,因为扫描器没有业务上下文,不知道orderId=1002和orderId=1001是不是同一个人的数据。这类漏洞往往需要人工结合业务逻辑去试,于是就成了团队之间的“灯下黑”——大家默认“扫描器没扫出来就等于没有”,结果一人工测试就翻车。2. 把概念说透:访问控制控的到底是什么2.1 认证与授权:先分清“你是谁”和“你能干什么”要理解Broken Access Control,必须先分清两个经常被混淆的概念:认证(Authentication)和授权(Authorization)。简单粗暴地说,认证解决的是“你是谁”的问题,常见手段是账号密码、短信验证码、扫码登录,成功后系统给你一个登录态;授权解决的是“你能干什么”的问题,它决定了你登录之后能看哪些页面、操作哪些按钮、读写哪些数据。对比项认证(Authentication)授权(Authorization)回答的问题你是谁?你能干什么?典型手段登录、Session、TokenRBAC角色、权限点、数据范围规则失败后果身份被冒用越权访问、数据泄露生活类比身份证验证你本人门禁卡决定你能进哪栋楼、哪层楼不少系统翻车就翻在“只做了认证,没做授权”。用户登录成功了,后端就让请求通过,至于这个用户有没有权限看某条订单、调某个管理接口,完全没管。这就好比大门口查了身份证,但进了大门之后每间办公室的门都敞着,谁都能进。2.2 三种访问控制模型:从RBAC到细粒度授权访问控制的落地模型,按粒度从粗到细大概有这几种。RBAC(基于角色的访问控制)是目前最常见的方式:给用户挂角色,给角色挂权限。优点是管理简单,缺点是数据范围很难表达。比如“销售经理可以查看所有订单”这个角色好定义,但“销售经理只能查看自己团队的订单”就没办法用纯RBAC描述,得加数据范围规则。ABAC(基于属性的访问控制)则把判断条件扩展到用户属性、资源属性、环境属性。比如“只有部门=销售部 且 时间=工作时间 且 资源归属=本部门”才允许访问。ABAC灵活,但规则复杂,维护成本高。对象级授权(Object-Level Authorization)是最容易被忽略的一层。角色权限管住了“能不能调用这个接口”,但没管住“能不能访问这一条数据”。比如普通用户能调用“查看订单”接口,但orderId=1002这条订单是否属于当前用户,必须单独校验。这也是IDOR类漏洞的核心病灶。选型建议:中小项目从RBAC起步完全够用,但无论选哪种模型,都必须想清楚一个问题——“数据范围”这个维度由谁、在哪一层来控制。想不清楚,后面必然出越权。2.3 前后端各管一摊:谁才是最后闸门还有一个特别常见的误区,就是以为前端不显示某个按钮,用户就“没有权限”了。我见过一个真实场景:系统根据登录用户的角色,在前端JS里判断是否渲染“删除”按钮。管理员能看到,普通用户看不到。大家都以为这就安全了,直到有人抓包直接发了一个DELETE请求,后端没有任何角色校验,数据就被删了。记住一句话:前端做的一切权限控制,都只是“用户体验层面的隐藏”,不是真正的安全控制。真正的权限闸门必须放在后端。后端应该在每个受保护接口上执行认证和授权校验,并且不信任任何来自前端的角色、身份、权限标记。把这句话刻在代码评审清单里,能少踩很多坑。3. 最常见的四类“破门”方式3.1 IDOR:换个ID号,别人的数据就漏出来了IDOR全称是Insecure Direct Object Reference,翻译过来是“不安全的直接对象引用”。它是Broken Access Control家族里出现频率最高的一类,也是新手最好理解的一类。场景是这样的:某个订单详情的请求长这样:GET /api/order/detail?orderId=2001 Authorization: Bearer xxx后端拿到orderId之后,直接从数据库查出订单返回,完全没校验这个订单是不是当前用户的。于是只要把orderId改成2002、2003,就能看到别人的订单。这类漏洞高发在自增主键的业务系统里。订单号、用户ID、文件ID、审批单号全是连续整数,遍历起来毫不费力,攻击者写个脚本从1数到十万,就可能批量拖走大量数据。这也是为什么很多安全评估报告里IDOR经常被评为高危——攻击门槛太低,影响面太大。3.2 功能级越权:普通账号发个管理员接口的请求功能级越权又叫垂直越权,指的是低权限用户访问了高权限用户才能使用的功能。典型的例子:普通用户在页面上找不到“用户管理”入口,但通过查看前端打包的JS文件,或者通过Swagger文档,发现了/api/admin/user/list这个接口。然后他用普通用户的Token直接请求这个接口,后端只校验了“是否登录”,没校验“是否管理员”,接口直接返回了全量用户列表。这类问题高发于“管理后台和用户端共用一套服务”的系统。开发者图省事,把管理接口和普通接口放在同一个服务、同一个路由域里,却忘了给管理接口加角色校验。于是路径一猜就中,权限一个没有。3.3 水平越权:同级用户之间的“互抄作业”水平越权指的是相同权限级别的用户之间,互相访问对方的数据。它和垂直越权的区别,我用个表列清楚:对比项垂直越权水平越权攻击者身份低权限用户同级用户攻击目标高权限功能同级用户的数据典型例子普通用户调用管理接口员工A查看员工B的薪资单根因缺少角色校验缺少对象归属校验水平越权比垂直越权更隐蔽。因为“调用这个接口”本身是允许的,只是“访问这条数据”不允许。很多团队做权限设计时只考虑了“谁能调用接口”,完全没想“谁能看这条数据”,导致水平越权大量存在。3.4 影子接口与目录遍历:没人记得的入口也是风险除了上面三类,还有一些边边角角的访问控制问题容易被忽略。影子接口。开发过程中临时加的调试接口、测试接口,上线后忘了删。这类接口往往没有权限校验,路径还特别容易猜,比如/api/test、/api/debug、/api/v1/internal。HTTP方法篡改。后端对接口的请求方法管控不严,比如本应只允许GET的接口,改成PUT也能执行更新操作。攻击者通过方法切换,绕过了那些“只拦GET不拦POST”的WAF规则。静态资源目录泄露。服务器配置不当,直接允许目录列举,备份文件、源代码、配置文件被直接下载。这些问题单独拿出来都不复杂,但叠加在一起,就会让系统的攻击面变得非常大。做安全评估时我习惯把“接口清单”和“权限矩阵”先拉出来,看看有多少接口没人认领、多少接口没有挂权限注解,往往一拉就发现问题。4. 一个模拟案例:从一份订单开始,把越权链路走了一遍4.1 场景设定:某公司内部订单系统为了把越权问题讲具体,我模拟一个典型的内部系统:某公司的订单管理系统。系统里有两类用户:普通员工(可以查看和提交自己的订单)和管理员(可以查看所有订单、修改订单状态)。系统使用自增订单号,登录后通过JWT标识用户身份。所有测试操作都发生在该公司授权的安全评估范围内,这是前提。没有授权的情况下拿别人系统做测试,那是违法行为,不在本文讨论范围内。4.2 越权测试的完整路径记录整个测试从一次“手贱”开始。我用普通员工账号登录系统,打开一条自己的订单,地址栏请求长这样:GET /api/order/detail?orderId=1001后端返回订单详情,一切正常。然后我把orderId改成了1002,按了一下回车,结果返回了另一个人的订单,包括收货地址和联系电话。这一步确认了IDOR存在之后,我没有停手,而是按下面的路径继续往下走:枚举订单ID。用脚本依次请求orderId=1001到1100,统计返回成功的数量,确认数据泄露范围。这一步很关键,能从“单个漏洞”上升到“批量数据泄露”。查看前端JS文件。从页面里拉出所有API路径,发现除了/api/order/detail,还有/api/order/updateStatus、/api/order/export、/api/admin/order/list等接口。尝试功能级越权。用普通员工Token直接请求/api/admin/order/list,后端返回200,并返回了全量订单列表。确认垂直越权存在。尝试方法篡改。/api/order/updateStatus只允许POST,我改成PUT请求,后端依然正常执行,说明方法级管控缺失。尝试未登录访问。把Authorization头去掉,直接请求/api/order/detail?orderId=1003,发现接口返回401,说明登录态校验是有的,问题就出在登录之后的授权环节。这一步走下来,一个问题清单已经成型:IDOR、垂直越权、方法级管控缺失,一个系统里同时存在三个访问控制缺陷。4.3 危害量化与风险评估很多团队拿到安全问题后,第一反应是“能不能上线”,而不是“这个问题有多大”。我习惯用类似CVSS的维度去量化。攻击路径是网络远程,攻击复杂度极低(改一个数字),无需任何特殊权限,攻击者只需一个普通账号,就能读取任意订单的敏感数据,甚至通过管理接口修改订单状态。数据保密性和完整性都受影响。套到通用评分体系里,这个组合通常在8分以上,属于高危。影响范围也很清楚:订单信息泄露是直接的,再往深挖,还能通过订单里的用户手机号做二次钓鱼,通过管理接口篡改订单状态影响业务流程。所以我在评估报告里给这个系统的结论是:上线前必修,且必须全校验,不能只修一个IDOR就收工。5. 测试方法论:一套可复用的越权排查流程5.1 测试前的账号与工具准备越权测试和普通漏洞扫描不太一样,它要求测试者具备一定的业务理解,并且需要准备好几样东西。账号方面,至少准备三个:普通用户A、普通用户B(互相看不到对方数据)、管理员账号。如果系统支持多租户,还要准备不同租户的账号。工具方面,我会准备Burp Suite抓包改包,Postman或curl批量发请求。浏览器里建议开两个独立的Profile,分别登录A和B,方便快速切换身份。所有操作都记录下来,后面写报告时直接引用。5.2 五步排查法,照做就行我给自己定了一套五步排查法,基本覆盖了越权测试的主要方向。第一步,梳理接口清单。从页面抓包、JS文件、Swagger/OpenAPI文档里把所有接口收集起来,整理成一张表,标出接口路径、请求方法、参数、对应的功能模块。没有接口清单,后面的测试就是无头苍蝇。第二步,枚举对象ID。针对带有对象参数的接口(订单、用户、文件、消息、发票等),逐个替换参数值,观察返回结果。重点记录“我本不该访问却访问成功”的对象。第三步,构造跨角色请求。把低权限账号的请求,替换成高权限账号的Token或Cookie,看后端是否校验角色。也可以反过来,用管理员Token请求普通接口,检查是否过度授权(虽然风险等级低,但也值得记录)。第四步,检查方法级权限。同一URL,依次尝试GET、POST、PUT、DELETE、PATCH,看后端对不同方法的处理是否一致。很多系统的权限注解只挂在某个方法上,换个方法就漏了。第五步,检查未授权路径与静态资源。尝试直接访问管理路径,比如/admin、/manager、/backup,以及静态文件比如.git、.env、backup.zip。这一步不用登录,纯粹看配置问题。5.3 自动化与半自动化辅助越权测试里,纯手工很容易漏,纯自动化又容易误报,我的做法是“脚本跑批量+人工看逻辑”。枚举ID阶段最适合写脚本。拿Python的requests库写个循环,把orderId从1001跑到2000,记录每个请求的状态码、响应包大小、响应里的关键字段,最后找规律。哪个ID返回200、响应体明显更长,就标记出来人工复核。还有个思路是把越权检测做成Burp Suite的扩展插件,拦截每个响应,自动用另一个账号的Token重放一遍,比对响应差异。不过这类工具需要二次开发,而且业务越权往往伴随复杂的业务状态变迁,纯自动化很容易漏报,只能当辅助。5.4 一份可以抄进工单里的测试用例模板越权测试的结论要能落地,格式很重要。我常用的模板长这样:模块接口路径涉及参数身份A请求身份B请求预期结果实际结果风险等级备注订单模块/api/order/detailorderIdA访问B的单号B访问A的单号均403200返回数据高IDOR管理模块/api/admin/order/list无普通用户请求管理员请求普通用户403200返回全量高垂直越权导出模块/api/order/exportorderId无登录态请求有登录态请求未登录401401低登录校验正常这份表的好处是,安全团队拿到后可以直接看到“预期和实际”的差异,开发团队也能快速定位是哪个接口的问题。真正修的时候,直接对着表一个个改就行。6. 加固方案:让访问控制回归“默认拒绝”6.1 后端强制校验:注解、中间件与过滤器修复越权问题,最核心的思路只有一个:把访问控制的判断全部放到后端,并且默认拒绝一切未明确授权的请求。Java/Spring项目里,可以用Spring Security的注解直接声明权限:@PreAuthorize("hasRole('ADMIN')") @GetMapping("/api/admin/order/list") public List<Order> listAllOrders() { return orderService.findAll(); }对象级归属校验同样可以在注解里写,但更推荐在服务层做。因为对象归属往往依赖业务规则,比如“订单归属人是当前用户”或者“当前用户所属团队是订单创建团队”:public Order getOrderDetail(Long orderId, User currentUser) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new ResourceNotFoundException()); if (!order.getOwnerId().equals(currentUser.getId()) && !currentUser.isAdmin()) { throw new ForbiddenException("无权访问该订单"); } return order; }Python的Flask项目,可以用装饰器把“对象归属校验”做成通用能力:def require_owner(model_cls): def decorator(f): @wraps(f) def wrapped(*args, **kwargs): obj = model_cls.query.get_or_404(kwargs.get("rid")) if obj.owner_id != session.get("user_id") and not session.get("is_admin"): abort(403) return f(*args, **kwargs) return wrapped return decoratorNode.js + Express项目,中间件是天然的控制位置:function requireRoles(roles = []) { return (req, res, next) => { if (!req.user) return res.status(401).json({ error: 'unauthorized' }); if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'forbidden' }); } next(); }; } app.get('/api/admin/order/list', requireRoles(['admin']), handler);这三段代码的共同点是:无论前端传什么,后端都用自己的数据源(数据库、Session、Token解析结果)来判断身份、角色、归属。这也是修复越权的唯一可靠路径。6.2 接口设计的减法:减少可猜测引用除了加校验,还可以从接口设计层面减少漏洞暴露面。第一个改动建议:把自增数字ID换成UUID或雪花ID。这能让攻击者“遍历ID”的成本变高,因为UUID无法顺序枚举。但必须强调,UUID只是增加了猜测难度,不是访问控制本身。网上有个经典误读是“我们用了UUID就安全了”,实际上如果接口不校验归属,换个UUID照样能泄露数据。第二个改动建议:敏感操作不要直接暴露对象ID。比如“删除订单”可以设计成“基于当前用户筛选后再删除”:await Order.destroy({ where: { id: orderId, ownerId: req.user.id } });任何查询都强制带上当前用户的条件,这是最朴素也最有效的兜底。就算漏了显式校验,数据库查询也会把数据范围限制住。6.3 用日志和监控兜底异常行为访问控制的加固不能只靠代码,还得靠监控。因为攻击者的越权尝试往往有规律可循,日志就是第一道警报。建议至少记录以下字段:用户ID、操作时间、接口路径、请求方法、资源ID、响应状态码。然后针对“短时间内大量请求不同对象ID”“频繁触发403/404”“异常对象ID访问”等行为设置告警规则。比如一个普通用户一小时内请求了上千个不同的orderId,就算全是403,也可能是攻击者正在尝试越权。再比如正常用户不会去访问/admin路径,突然出现大量这类请求,就该触发告警。6.4 CI/CD阶段就把越权问题拦下来越权修复不能只靠上线后的渗透测试,最好前移到开发和发布阶段。代码评审阶段,我建议把“这个接口有没有做授权校验、有没有做对象归属校验”写进Checklist,每一条接口变更都要过一遍。CI流水线里可以挂SAST扫描,自动扫描“存在路由但缺少权限注解”的接口,生成报告同步给开发。更有效的做法是把越权测试用例沉淀成自动化集成测试。比如在JUnit或Pytest里构造“低权限账号请求高权限接口”的用例,断言返回403。这类用例一旦失败,流水线直接红灯,问题在merge前就暴露了,而不是等安全团队上线后才测出来。7. 实战翻车点与避坑实录7.1 五个高频踩坑点速查我把这些年实际踩过、也看别人踩过的坑整理成了一张速查表:坑点表现根因规避方法以为UUID就安全接口仍然能通过UUID泄露数据混淆了“不可猜测”和“不可访问”明确UUID只是辅助,后端仍然要校验归属只校验登录状态所有登录用户能调管理接口授权校验缺失在中间件/注解层统一增加角色校验前端隐藏按钮当权限抓包手动请求仍能执行操作前端控制不是安全控制后端强制执行权限判断服务间调用信任上游请求头内部服务绕过权限校验默认信任内网/上游服务间调用使用独立身份认证,不盲目信任传递的header测试只用单个账号水平越权完全测不出来测试设计缺陷至少双账号交叉测试,模拟真实用户互访7.2 最推荐的双账号联动测试在越权测试这件事上,我实践下来最稳的方法就是双账号联动测试。操作步骤:准备账号A和账号B,A是普通用户,B也是普通用户,两者数据完全隔离。用浏览器无痕窗口分别登录A和B,然后让A去访问B的资源对象,再让B去访问A的资源对象。加上未登录账号,形成一个简单的测试矩阵:访问者目标对象A目标对象B未登录401/403401/403账号A200允许必须403拒绝账号B必须403拒绝200允许如果“账号A访问目标对象B”返回了200,那水平越权实锤。如果“未登录访问”返回了200,那认证层都有问题。这个矩阵简单直观,我每次做安全评估都会先把它跑一遍。7.3 越权回归用例怎么设计修复之后还得防止回退。我把越权回归测试分成三类,只要有接口变更就触发:第一类是未登录访问受保护接口,断言401或403。第二类是低权限账号访问高权限接口,断言403。第三类是对象ID交叉访问,比如账号A访问目标对象B,断言403。这类用例写起来不复杂,比如JUnit里可以这样组织:@Test @WithMockUser(username = "userA", roles = {"USER"}) public void userAShouldNotAccessOrderOfUserB() { mockMvc.perform(get("/api/order/detail").param("orderId", "1002")) .andExpect(status().isForbidden()); }别小看这几条用例,它们成本低、覆盖面大,是防止越权问题“修了又复发”的保底手段。做了这么多年的安全评估,我自己最大的体会是:访问控制类的漏洞,大多不是方案多难,而是“默认信任”这四个字在作祟。默认信任前端、默认信任登录态、默认信任内部服务,信任一层层堆上去,漏洞就一层层漏下来。修复的功夫反而简单,就是把信任全部收回来,每个请求都重新问一遍:你是谁,你凭什么访问这条数据。最后再分享一个我一直在用的习惯:写查询逻辑时,永远把当前用户ID作为查询条件的一部分拼进SQL或ORM入参里,让数据库层面的数据范围天然受限。这一招看着笨,但它能兜住绝大多数你没想到的越权路径。
做Web安全这些年,我见过太多团队把OWASP TOP10挂在嘴边,可真要较真问一句“Broken Access Control到底怎么复现、怎么修、怎么防止下次再犯”,会议室里往往就安静了。正巧最近在重翻这个系列的笔记,聊到榜单上这个常年霸榜的老熟人——2017版里它排在A5,到了2021版直接冲到A01,中文名叫“访问控制失效”。名字听着挺技术,说白了就是:该管住的地方没管住,不该让人访问的数据被翻出来了。这篇我打算把它的原理、高频漏洞形态、测试套路、加固代码一次讲透,不管你是开发、测试、安全工程师,还是被“改几行代码就上线”催着走的技术负责人,都能照着落地。1. 榜单上的“常驻嘉宾”:A5-Broken Access Control到底有多严重1.1 从A5到A01:排名变化背后的信号OWASP TOP10是Web安全领域公认的“风险排行榜”,每三四年更新一次,依据来自全球数百家机构提交的真实漏洞数据。很多人只记了个榜单名称,没细看排名变化背后的含义。2017版里Broken Access Control排在第5位,到了2021版直接升到第1位,这不是什么数据抖动,而是统计口径里这类漏洞的样本量实在太大了,大到把榜单头部整个占住。为什么样本量会这么大?因为“访问控制失效”从来不是一个单一漏洞,而是一个漏洞家族。官方在统计时把它对应的CWE都挂了进去,包括CWE-862(缺失授权)、CWE-863(授权不正确)、CWE-639(通过用户可控键绕过授权)、CWE-352(跨站请求伪造)等一堆子类型。换句话说,你只要在业务里出现“用户能访问本不该访问的数据”这类现象,都可能被归到这个家族里。这也解释了为什么这期系列我觉得值得单独拿出来写——它不是某个冷门技术点,而是每一家企业都有可能踩中的普遍性问题。1.2 为什么越权问题这么普遍我复盘过不少安全评估项目,越权类漏洞几乎霸占了中高危漏洞排查单的前排。它这么普遍,绕不开三个根因。第一,业务复杂。一个系统里光角色就有普通用户、管理员、审计员、客服,再加上多租户、多组织、多项目的数据范围,权限模型稍微设计得不严谨,就会出现“管住了角色、没管住数据范围”的漏洞。第二,开发习惯。很多接口开发时的惯性思维是“能拿到数据就行”,只在入口处校验了“是否登录”,忘了校验“是否有权限看这条数据”。更常见的是,后端默认信任前端传来的用户身份字段,比如把user_id放在请求体里,后端直接拿它当当前用户,这等于把门锁钥匙挂在门框上。第三,安全测试有盲区。传统的自动化扫描器很难发现越权漏洞,因为扫描器没有业务上下文,不知道orderId=1002和orderId=1001是不是同一个人的数据。这类漏洞往往需要人工结合业务逻辑去试,于是就成了团队之间的“灯下黑”——大家默认“扫描器没扫出来就等于没有”,结果一人工测试就翻车。2. 把概念说透:访问控制控的到底是什么2.1 认证与授权:先分清“你是谁”和“你能干什么”要理解Broken Access Control,必须先分清两个经常被混淆的概念:认证(Authentication)和授权(Authorization)。简单粗暴地说,认证解决的是“你是谁”的问题,常见手段是账号密码、短信验证码、扫码登录,成功后系统给你一个登录态;授权解决的是“你能干什么”的问题,它决定了你登录之后能看哪些页面、操作哪些按钮、读写哪些数据。对比项认证(Authentication)授权(Authorization)回答的问题你是谁?你能干什么?典型手段登录、Session、TokenRBAC角色、权限点、数据范围规则失败后果身份被冒用越权访问、数据泄露生活类比身份证验证你本人门禁卡决定你能进哪栋楼、哪层楼不少系统翻车就翻在“只做了认证,没做授权”。用户登录成功了,后端就让请求通过,至于这个用户有没有权限看某条订单、调某个管理接口,完全没管。这就好比大门口查了身份证,但进了大门之后每间办公室的门都敞着,谁都能进。2.2 三种访问控制模型:从RBAC到细粒度授权访问控制的落地模型,按粒度从粗到细大概有这几种。RBAC(基于角色的访问控制)是目前最常见的方式:给用户挂角色,给角色挂权限。优点是管理简单,缺点是数据范围很难表达。比如“销售经理可以查看所有订单”这个角色好定义,但“销售经理只能查看自己团队的订单”就没办法用纯RBAC描述,得加数据范围规则。ABAC(基于属性的访问控制)则把判断条件扩展到用户属性、资源属性、环境属性。比如“只有部门=销售部 且 时间=工作时间 且 资源归属=本部门”才允许访问。ABAC灵活,但规则复杂,维护成本高。对象级授权(Object-Level Authorization)是最容易被忽略的一层。角色权限管住了“能不能调用这个接口”,但没管住“能不能访问这一条数据”。比如普通用户能调用“查看订单”接口,但orderId=1002这条订单是否属于当前用户,必须单独校验。这也是IDOR类漏洞的核心病灶。选型建议:中小项目从RBAC起步完全够用,但无论选哪种模型,都必须想清楚一个问题——“数据范围”这个维度由谁、在哪一层来控制。想不清楚,后面必然出越权。2.3 前后端各管一摊:谁才是最后闸门还有一个特别常见的误区,就是以为前端不显示某个按钮,用户就“没有权限”了。我见过一个真实场景:系统根据登录用户的角色,在前端JS里判断是否渲染“删除”按钮。管理员能看到,普通用户看不到。大家都以为这就安全了,直到有人抓包直接发了一个DELETE请求,后端没有任何角色校验,数据就被删了。记住一句话:前端做的一切权限控制,都只是“用户体验层面的隐藏”,不是真正的安全控制。真正的权限闸门必须放在后端。后端应该在每个受保护接口上执行认证和授权校验,并且不信任任何来自前端的角色、身份、权限标记。把这句话刻在代码评审清单里,能少踩很多坑。3. 最常见的四类“破门”方式3.1 IDOR:换个ID号,别人的数据就漏出来了IDOR全称是Insecure Direct Object Reference,翻译过来是“不安全的直接对象引用”。它是Broken Access Control家族里出现频率最高的一类,也是新手最好理解的一类。场景是这样的:某个订单详情的请求长这样:GET /api/order/detail?orderId=2001 Authorization: Bearer xxx后端拿到orderId之后,直接从数据库查出订单返回,完全没校验这个订单是不是当前用户的。于是只要把orderId改成2002、2003,就能看到别人的订单。这类漏洞高发在自增主键的业务系统里。订单号、用户ID、文件ID、审批单号全是连续整数,遍历起来毫不费力,攻击者写个脚本从1数到十万,就可能批量拖走大量数据。这也是为什么很多安全评估报告里IDOR经常被评为高危——攻击门槛太低,影响面太大。3.2 功能级越权:普通账号发个管理员接口的请求功能级越权又叫垂直越权,指的是低权限用户访问了高权限用户才能使用的功能。典型的例子:普通用户在页面上找不到“用户管理”入口,但通过查看前端打包的JS文件,或者通过Swagger文档,发现了/api/admin/user/list这个接口。然后他用普通用户的Token直接请求这个接口,后端只校验了“是否登录”,没校验“是否管理员”,接口直接返回了全量用户列表。这类问题高发于“管理后台和用户端共用一套服务”的系统。开发者图省事,把管理接口和普通接口放在同一个服务、同一个路由域里,却忘了给管理接口加角色校验。于是路径一猜就中,权限一个没有。3.3 水平越权:同级用户之间的“互抄作业”水平越权指的是相同权限级别的用户之间,互相访问对方的数据。它和垂直越权的区别,我用个表列清楚:对比项垂直越权水平越权攻击者身份低权限用户同级用户攻击目标高权限功能同级用户的数据典型例子普通用户调用管理接口员工A查看员工B的薪资单根因缺少角色校验缺少对象归属校验水平越权比垂直越权更隐蔽。因为“调用这个接口”本身是允许的,只是“访问这条数据”不允许。很多团队做权限设计时只考虑了“谁能调用接口”,完全没想“谁能看这条数据”,导致水平越权大量存在。3.4 影子接口与目录遍历:没人记得的入口也是风险除了上面三类,还有一些边边角角的访问控制问题容易被忽略。影子接口。开发过程中临时加的调试接口、测试接口,上线后忘了删。这类接口往往没有权限校验,路径还特别容易猜,比如/api/test、/api/debug、/api/v1/internal。HTTP方法篡改。后端对接口的请求方法管控不严,比如本应只允许GET的接口,改成PUT也能执行更新操作。攻击者通过方法切换,绕过了那些“只拦GET不拦POST”的WAF规则。静态资源目录泄露。服务器配置不当,直接允许目录列举,备份文件、源代码、配置文件被直接下载。这些问题单独拿出来都不复杂,但叠加在一起,就会让系统的攻击面变得非常大。做安全评估时我习惯把“接口清单”和“权限矩阵”先拉出来,看看有多少接口没人认领、多少接口没有挂权限注解,往往一拉就发现问题。4. 一个模拟案例:从一份订单开始,把越权链路走了一遍4.1 场景设定:某公司内部订单系统为了把越权问题讲具体,我模拟一个典型的内部系统:某公司的订单管理系统。系统里有两类用户:普通员工(可以查看和提交自己的订单)和管理员(可以查看所有订单、修改订单状态)。系统使用自增订单号,登录后通过JWT标识用户身份。所有测试操作都发生在该公司授权的安全评估范围内,这是前提。没有授权的情况下拿别人系统做测试,那是违法行为,不在本文讨论范围内。4.2 越权测试的完整路径记录整个测试从一次“手贱”开始。我用普通员工账号登录系统,打开一条自己的订单,地址栏请求长这样:GET /api/order/detail?orderId=1001后端返回订单详情,一切正常。然后我把orderId改成了1002,按了一下回车,结果返回了另一个人的订单,包括收货地址和联系电话。这一步确认了IDOR存在之后,我没有停手,而是按下面的路径继续往下走:枚举订单ID。用脚本依次请求orderId=1001到1100,统计返回成功的数量,确认数据泄露范围。这一步很关键,能从“单个漏洞”上升到“批量数据泄露”。查看前端JS文件。从页面里拉出所有API路径,发现除了/api/order/detail,还有/api/order/updateStatus、/api/order/export、/api/admin/order/list等接口。尝试功能级越权。用普通员工Token直接请求/api/admin/order/list,后端返回200,并返回了全量订单列表。确认垂直越权存在。尝试方法篡改。/api/order/updateStatus只允许POST,我改成PUT请求,后端依然正常执行,说明方法级管控缺失。尝试未登录访问。把Authorization头去掉,直接请求/api/order/detail?orderId=1003,发现接口返回401,说明登录态校验是有的,问题就出在登录之后的授权环节。这一步走下来,一个问题清单已经成型:IDOR、垂直越权、方法级管控缺失,一个系统里同时存在三个访问控制缺陷。4.3 危害量化与风险评估很多团队拿到安全问题后,第一反应是“能不能上线”,而不是“这个问题有多大”。我习惯用类似CVSS的维度去量化。攻击路径是网络远程,攻击复杂度极低(改一个数字),无需任何特殊权限,攻击者只需一个普通账号,就能读取任意订单的敏感数据,甚至通过管理接口修改订单状态。数据保密性和完整性都受影响。套到通用评分体系里,这个组合通常在8分以上,属于高危。影响范围也很清楚:订单信息泄露是直接的,再往深挖,还能通过订单里的用户手机号做二次钓鱼,通过管理接口篡改订单状态影响业务流程。所以我在评估报告里给这个系统的结论是:上线前必修,且必须全校验,不能只修一个IDOR就收工。5. 测试方法论:一套可复用的越权排查流程5.1 测试前的账号与工具准备越权测试和普通漏洞扫描不太一样,它要求测试者具备一定的业务理解,并且需要准备好几样东西。账号方面,至少准备三个:普通用户A、普通用户B(互相看不到对方数据)、管理员账号。如果系统支持多租户,还要准备不同租户的账号。工具方面,我会准备Burp Suite抓包改包,Postman或curl批量发请求。浏览器里建议开两个独立的Profile,分别登录A和B,方便快速切换身份。所有操作都记录下来,后面写报告时直接引用。5.2 五步排查法,照做就行我给自己定了一套五步排查法,基本覆盖了越权测试的主要方向。第一步,梳理接口清单。从页面抓包、JS文件、Swagger/OpenAPI文档里把所有接口收集起来,整理成一张表,标出接口路径、请求方法、参数、对应的功能模块。没有接口清单,后面的测试就是无头苍蝇。第二步,枚举对象ID。针对带有对象参数的接口(订单、用户、文件、消息、发票等),逐个替换参数值,观察返回结果。重点记录“我本不该访问却访问成功”的对象。第三步,构造跨角色请求。把低权限账号的请求,替换成高权限账号的Token或Cookie,看后端是否校验角色。也可以反过来,用管理员Token请求普通接口,检查是否过度授权(虽然风险等级低,但也值得记录)。第四步,检查方法级权限。同一URL,依次尝试GET、POST、PUT、DELETE、PATCH,看后端对不同方法的处理是否一致。很多系统的权限注解只挂在某个方法上,换个方法就漏了。第五步,检查未授权路径与静态资源。尝试直接访问管理路径,比如/admin、/manager、/backup,以及静态文件比如.git、.env、backup.zip。这一步不用登录,纯粹看配置问题。5.3 自动化与半自动化辅助越权测试里,纯手工很容易漏,纯自动化又容易误报,我的做法是“脚本跑批量+人工看逻辑”。枚举ID阶段最适合写脚本。拿Python的requests库写个循环,把orderId从1001跑到2000,记录每个请求的状态码、响应包大小、响应里的关键字段,最后找规律。哪个ID返回200、响应体明显更长,就标记出来人工复核。还有个思路是把越权检测做成Burp Suite的扩展插件,拦截每个响应,自动用另一个账号的Token重放一遍,比对响应差异。不过这类工具需要二次开发,而且业务越权往往伴随复杂的业务状态变迁,纯自动化很容易漏报,只能当辅助。5.4 一份可以抄进工单里的测试用例模板越权测试的结论要能落地,格式很重要。我常用的模板长这样:模块接口路径涉及参数身份A请求身份B请求预期结果实际结果风险等级备注订单模块/api/order/detailorderIdA访问B的单号B访问A的单号均403200返回数据高IDOR管理模块/api/admin/order/list无普通用户请求管理员请求普通用户403200返回全量高垂直越权导出模块/api/order/exportorderId无登录态请求有登录态请求未登录401401低登录校验正常这份表的好处是,安全团队拿到后可以直接看到“预期和实际”的差异,开发团队也能快速定位是哪个接口的问题。真正修的时候,直接对着表一个个改就行。6. 加固方案:让访问控制回归“默认拒绝”6.1 后端强制校验:注解、中间件与过滤器修复越权问题,最核心的思路只有一个:把访问控制的判断全部放到后端,并且默认拒绝一切未明确授权的请求。Java/Spring项目里,可以用Spring Security的注解直接声明权限:@PreAuthorize("hasRole('ADMIN')") @GetMapping("/api/admin/order/list") public List<Order> listAllOrders() { return orderService.findAll(); }对象级归属校验同样可以在注解里写,但更推荐在服务层做。因为对象归属往往依赖业务规则,比如“订单归属人是当前用户”或者“当前用户所属团队是订单创建团队”:public Order getOrderDetail(Long orderId, User currentUser) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new ResourceNotFoundException()); if (!order.getOwnerId().equals(currentUser.getId()) && !currentUser.isAdmin()) { throw new ForbiddenException("无权访问该订单"); } return order; }Python的Flask项目,可以用装饰器把“对象归属校验”做成通用能力:def require_owner(model_cls): def decorator(f): @wraps(f) def wrapped(*args, **kwargs): obj = model_cls.query.get_or_404(kwargs.get("rid")) if obj.owner_id != session.get("user_id") and not session.get("is_admin"): abort(403) return f(*args, **kwargs) return wrapped return decoratorNode.js + Express项目,中间件是天然的控制位置:function requireRoles(roles = []) { return (req, res, next) => { if (!req.user) return res.status(401).json({ error: 'unauthorized' }); if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'forbidden' }); } next(); }; } app.get('/api/admin/order/list', requireRoles(['admin']), handler);这三段代码的共同点是:无论前端传什么,后端都用自己的数据源(数据库、Session、Token解析结果)来判断身份、角色、归属。这也是修复越权的唯一可靠路径。6.2 接口设计的减法:减少可猜测引用除了加校验,还可以从接口设计层面减少漏洞暴露面。第一个改动建议:把自增数字ID换成UUID或雪花ID。这能让攻击者“遍历ID”的成本变高,因为UUID无法顺序枚举。但必须强调,UUID只是增加了猜测难度,不是访问控制本身。网上有个经典误读是“我们用了UUID就安全了”,实际上如果接口不校验归属,换个UUID照样能泄露数据。第二个改动建议:敏感操作不要直接暴露对象ID。比如“删除订单”可以设计成“基于当前用户筛选后再删除”:await Order.destroy({ where: { id: orderId, ownerId: req.user.id } });任何查询都强制带上当前用户的条件,这是最朴素也最有效的兜底。就算漏了显式校验,数据库查询也会把数据范围限制住。6.3 用日志和监控兜底异常行为访问控制的加固不能只靠代码,还得靠监控。因为攻击者的越权尝试往往有规律可循,日志就是第一道警报。建议至少记录以下字段:用户ID、操作时间、接口路径、请求方法、资源ID、响应状态码。然后针对“短时间内大量请求不同对象ID”“频繁触发403/404”“异常对象ID访问”等行为设置告警规则。比如一个普通用户一小时内请求了上千个不同的orderId,就算全是403,也可能是攻击者正在尝试越权。再比如正常用户不会去访问/admin路径,突然出现大量这类请求,就该触发告警。6.4 CI/CD阶段就把越权问题拦下来越权修复不能只靠上线后的渗透测试,最好前移到开发和发布阶段。代码评审阶段,我建议把“这个接口有没有做授权校验、有没有做对象归属校验”写进Checklist,每一条接口变更都要过一遍。CI流水线里可以挂SAST扫描,自动扫描“存在路由但缺少权限注解”的接口,生成报告同步给开发。更有效的做法是把越权测试用例沉淀成自动化集成测试。比如在JUnit或Pytest里构造“低权限账号请求高权限接口”的用例,断言返回403。这类用例一旦失败,流水线直接红灯,问题在merge前就暴露了,而不是等安全团队上线后才测出来。7. 实战翻车点与避坑实录7.1 五个高频踩坑点速查我把这些年实际踩过、也看别人踩过的坑整理成了一张速查表:坑点表现根因规避方法以为UUID就安全接口仍然能通过UUID泄露数据混淆了“不可猜测”和“不可访问”明确UUID只是辅助,后端仍然要校验归属只校验登录状态所有登录用户能调管理接口授权校验缺失在中间件/注解层统一增加角色校验前端隐藏按钮当权限抓包手动请求仍能执行操作前端控制不是安全控制后端强制执行权限判断服务间调用信任上游请求头内部服务绕过权限校验默认信任内网/上游服务间调用使用独立身份认证,不盲目信任传递的header测试只用单个账号水平越权完全测不出来测试设计缺陷至少双账号交叉测试,模拟真实用户互访7.2 最推荐的双账号联动测试在越权测试这件事上,我实践下来最稳的方法就是双账号联动测试。操作步骤:准备账号A和账号B,A是普通用户,B也是普通用户,两者数据完全隔离。用浏览器无痕窗口分别登录A和B,然后让A去访问B的资源对象,再让B去访问A的资源对象。加上未登录账号,形成一个简单的测试矩阵:访问者目标对象A目标对象B未登录401/403401/403账号A200允许必须403拒绝账号B必须403拒绝200允许如果“账号A访问目标对象B”返回了200,那水平越权实锤。如果“未登录访问”返回了200,那认证层都有问题。这个矩阵简单直观,我每次做安全评估都会先把它跑一遍。7.3 越权回归用例怎么设计修复之后还得防止回退。我把越权回归测试分成三类,只要有接口变更就触发:第一类是未登录访问受保护接口,断言401或403。第二类是低权限账号访问高权限接口,断言403。第三类是对象ID交叉访问,比如账号A访问目标对象B,断言403。这类用例写起来不复杂,比如JUnit里可以这样组织:@Test @WithMockUser(username = "userA", roles = {"USER"}) public void userAShouldNotAccessOrderOfUserB() { mockMvc.perform(get("/api/order/detail").param("orderId", "1002")) .andExpect(status().isForbidden()); }别小看这几条用例,它们成本低、覆盖面大,是防止越权问题“修了又复发”的保底手段。做了这么多年的安全评估,我自己最大的体会是:访问控制类的漏洞,大多不是方案多难,而是“默认信任”这四个字在作祟。默认信任前端、默认信任登录态、默认信任内部服务,信任一层层堆上去,漏洞就一层层漏下来。修复的功夫反而简单,就是把信任全部收回来,每个请求都重新问一遍:你是谁,你凭什么访问这条数据。最后再分享一个我一直在用的习惯:写查询逻辑时,永远把当前用户ID作为查询条件的一部分拼进SQL或ORM入参里,让数据库层面的数据范围天然受限。这一招看着笨,但它能兜住绝大多数你没想到的越权路径。