跳转到帖子

背景

越权漏洞在 Web 安全里并不新鲜,但在实际代码审计和业务测试中,它仍然是最容易被遗漏的一类问题。原因也很现实:权限判断往往散落在 Controller、Service、DAO,甚至前端路由里;业务迭代频繁后,原本清晰的权限模型会被临时需求逐渐冲散。

这篇文章整理一次典型的接口越权问题复盘,重点放在如何定位、如何验证、如何修复,以及后续代码审计时可以沉淀哪些检查方法。内容不涉及真实目标系统,也不提供攻击他人系统的操作引导,适合作为内部安全评审或代码审计 checklist 参考。

问题场景

某业务系统中有一个资源管理功能,用户可以查看、编辑自己所属组织下的资源。接口设计大致如下:

GET  /api/resource/detail?id=1001
POST /api/resource/update

前端页面会根据当前登录用户展示资源列表,用户点击某条记录后,前端把资源 id 带到详情接口。表面看起来没有问题,因为普通用户在页面上只能看到自己有权限的资源。

但问题出在后端接口只根据传入的 id 查询资源详情,没有校验该资源是否属于当前用户可访问范围。攻击者如果知道或猜到其他资源 id,就可能读取或修改非授权资源。

越权问题的核心不是“用户能不能看到按钮”,而是“后端在每一次访问资源时,是否重新验证主体、动作、资源三者之间的授权关系”。

技术分析

以常见的 Java Web 分层为例,问题代码可能类似下面这样:

// Controller
@GetMapping("/detail")
public ResourceDTO detail(@RequestParam Long id) {
    return resourceService.getDetail(id);
}

// Service
public ResourceDTO getDetail(Long id) {
    Resource resource = resourceMapper.selectById(id);
    return convert(resource);
}

这段代码的问题不是 SQL 注入,也不是认证缺失,而是认证后的授权缺失。用户已经登录,但系统没有确认该用户是否有权访问 id 对应的资源。

在很多系统里,权限校验通常分为两层:

  • 功能权限:当前用户是否可以访问某个菜单或接口,例如是否具备 resource:view 权限。
  • 数据权限:当前用户是否可以访问某条具体数据,例如资源是否属于其组织、项目或租户。

实际漏洞多出现在第二层。功能权限配置正确,接口也要求登录,但数据权限没有落到查询条件或业务判断里。

关键验证思路

在合法授权的测试环境中,可以按以下思路验证此类问题。这里强调,验证应限定在自有系统、授权测试环境或漏洞靶场中。

1. 确认资源标识是否可控

首先观察请求参数、路径参数或请求体中是否存在资源标识,例如:

id=1001
resourceId=1001
projectId=20
orderNo=202401010001

如果这些标识由客户端传入,就需要进一步确认后端是否以当前用户身份进行二次授权判断。

2. 使用两个低权限账号交叉验证

准备两个属于不同组织或不同项目的普通账号,分别创建或查看各自资源。然后在授权测试环境中,将 A 账号请求中的资源 id 替换为 B 账号资源 id,观察响应是否返回敏感数据或允许修改。

需要注意的是,判断越权不能只看 HTTP 状态码,还要看业务返回码、响应体字段、数据库状态变化以及审计日志。

3. 区分读越权和写越权

读接口常见问题是详情、导出、下载、预览接口缺少数据权限。写接口常见问题是更新、删除、审批、绑定关系接口只校验了对象存在性,没有校验对象归属。

例如下面这种写接口就比较危险:

public void update(ResourceUpdateRequest request) {
    Resource resource = resourceMapper.selectById(request.getId());
    if (resource == null) {
        throw new BizException("资源不存在");
    }
    resource.setName(request.getName());
    resourceMapper.updateById(resource);
}

它只判断资源是否存在,却没有判断当前用户是否能操作该资源。

修复设计

修复越权问题,不建议只在某个 Controller 上临时加 if 判断。更稳妥的做法是建立统一的数据权限校验模式,至少要覆盖查询、更新、删除、导出等入口。

1. 在查询条件中绑定用户可访问范围

对于列表和详情查询,可以把用户的数据范围加入 SQL 条件。示例:

public ResourceDTO getDetail(Long id, LoginUser user) {
    Resource resource = resourceMapper.selectByIdAndOrgIds(
        id,
        user.getAllowedOrgIds()
    );
    if (resource == null) {
        throw new BizException("资源不存在或无权限访问");
    }
    return convert(resource);
}

对应 SQL 可以类似:

SELECT *
FROM resource
WHERE id = #{id}
  AND org_id IN
  <foreach collection="orgIds" item="orgId" open="(" separator="," close=")">
    #{orgId}
  </foreach>

这里有一个细节:错误提示建议统一为“资源不存在或无权限访问”,避免通过差异化错误信息帮助枚举资源是否存在。

2. 写操作前做归属校验

更新、删除、审批类接口不要只根据 id 直接执行。可以先查询当前用户可操作范围内的对象,再进行修改:

public void update(ResourceUpdateRequest request, LoginUser user) {
    Resource resource = resourceMapper.selectByIdAndOrgIds(
        request.getId(),
        user.getAllowedOrgIds()
    );
    if (resource == null) {
        throw new BizException("资源不存在或无权限访问");
    }

    resource.setName(request.getName());
    resourceMapper.updateById(resource);
}

如果是高并发或状态敏感的操作,也可以把权限条件合并到 update 语句中,避免查询后对象状态变化带来的竞态问题:

UPDATE resource
SET name = #{name}, update_time = NOW()
WHERE id = #{id}
  AND org_id IN (...)
  AND status = 'DRAFT';

执行后检查影响行数,影响行为 0 时按无权限或状态不允许处理。

3. 避免只依赖前端控制

前端隐藏按钮、路由拦截、禁用输入框只能改善用户体验,不能作为安全边界。所有关键授权必须在服务端完成。尤其是管理后台、移动端接口、小程序接口,经常存在多个前端共用同一套后端接口的情况,不能假设前端一定按预期传参。

代码审计关注点

审计越权问题时,可以围绕“资源标识进入点”和“权限判断落点”做静态排查。

1. 搜索高风险接口命名

可以先从常见方法名入手:

detail
info
getById
update
delete
remove
export
download
approve
bind
unbind
assign

这些接口通常直接操作具体资源,若只看到 selectById、updateById、deleteById,就要重点确认是否有额外的数据权限判断。

2. 检查 Mapper 层是否包含租户或组织条件

多租户系统里,所有业务表一般都应该带 tenant_id、org_id、project_id 等隔离字段。审计时可以抽样检查核心表的查询语句是否包含这些约束。

检查对象重点关注
详情查询是否只按 id 查询
列表查询是否带租户、组织、项目范围
更新删除WHERE 条件是否只包含主键
导出下载是否复用列表权限条件
审批流是否校验当前处理人或角色

3. 关注间接对象引用

有些接口表面上没有 resourceId,但会通过其他字段间接关联资源,例如 orderNo、fileKey、taskId、messageId。只要这个字段能定位到具体业务对象,就需要做授权校验。

文件下载接口尤其常见:数据库中保存 file_id 和业务对象关系,但下载接口只根据 file_id 取文件,导致用户可以下载不属于自己的附件。修复时应校验文件所属业务对象是否在当前用户权限范围内。

测试与回归建议

越权漏洞修复后,建议补充自动化回归用例,避免后续迭代再次引入。最基本的用例应覆盖:

  • 同一用户访问自己的资源,预期成功。
  • 不同组织用户访问彼此资源,预期失败。
  • 无权限用户执行更新、删除、导出,预期失败。
  • 管理员或授权角色访问授权范围内资源,预期成功。
  • 不存在资源和无权限资源返回信息不泄露差异。

如果项目使用 Spring Security 或类似框架,可以把功能权限放在注解或网关层,把数据权限放在 Service 或 Mapper 层。两者不要混为一谈。

@PreAuthorize("hasAuthority('resource:view')")
@GetMapping("/detail")
public ResourceDTO detail(@RequestParam Long id) {
    LoginUser user = LoginContext.getUser();
    return resourceService.getDetail(id, user);
}

这里注解只解决“能不能访问详情功能”,Service 中仍然要解决“能不能访问这条数据”。

注意点

  • 不要把用户传入的 orgId、tenantId 当作可信数据,应从登录态或服务端上下文获取。
  • 不要用自增 id 是否难猜来代替权限校验,复杂 id 也不是授权机制。
  • 错误提示应避免暴露资源存在性,日志中可以记录更详细原因,但不要直接返回给客户端。
  • 批量操作要逐条或按权限范围过滤,不能只校验第一条数据。
  • 导出、下载、打印、分享链接等边缘接口要纳入同一套数据权限模型。

总结

越权漏洞的修复重点不在于某个接口加一行判断,而在于建立稳定的授权边界。简单说,就是每次访问具体资源时,都要确认当前主体是否有权对该资源执行该动作。

从代码审计角度看,优先关注按 id 查询、按 id 更新、文件下载、导出、审批流、绑定关系等接口,通常能较快发现问题。修复时建议把用户可访问范围下沉到 Service 或 Mapper 层,并通过测试用例固化下来。这样即使业务继续迭代,权限模型也不容易被局部改动破坏。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…