我扫了项目里 739 个 Controller,发现最危险的越权漏洞不是没认证

我扫了项目里 739 个 Controller,发现最危险的越权漏洞不是没认证

最近做了一次项目内审,结果有点出乎我的意料。

先说好消息:网关层的认证做得很完整------token 校验、白名单、XSS 过滤、跨域配置,一个不少。所有接口都需要登录,匿名接口寥寥无几且都有明确注释。

坏消息是:认证没问题,鉴权全是洞。

一组吓人的数字

我用 grep 扫了整个后端 739 个 Controller 文件,统计了两个注解的出现次数:

  • @RequiresPermissions(功能权限):542 处
  • @DataScope(数据权限):35 处

差了 15 倍。

什么意思呢?就是"你能不能进这个菜单"管得很严,但"进了菜单之后你能看哪些数据"几乎没人管。

一个最典型的例子

有个"学生详情"接口,路径长这样:

ini 复制代码
GET /student/detail?studentId=123

接口上有 @RequiresPermissions("student:view"),功能权限是有的。但你把 URL 里的 studentId 改成任意一个数字,都能看到对应学生的全部信息------包括身份证号、家庭住址、家长手机号。

因为代码里是这么写的:

java 复制代码
@GetMapping("/detail")
@RequiresPermissions("student:view")
public AjaxResult getInfo(Long studentId) {
    Student student = studentService.selectStudentById(studentId);
    return AjaxResult.success(student);
}

直接拿前端传进来的 studentId 去查库,完全没校验这个学生是不是当前登录用户管辖范围内的。

你是 A 班的老师,改一下 URL 里的 ID 就能看到 B 班 C 校 D 市的学生数据。

这就是典型的水平越权。

为什么会变成这样

我捋了一下,根因有三个:

1. 生成器的"遗产"

代码生成器出来的 CRUD Controller 都带权限注解,这没问题。但项目里手写的新模块、聚合接口、统计接口,是开发自己加的 Controller,这些大多漏了注解。

生成器管得了标准化的东西,管不了人手动写的。

2. 网关只认证、不鉴权

网关 Filter 只校验"token 有效吗",至于"这个用户能不能调这个接口、能看哪些数据",是各服务自己的注解说了算。

于是就出现了一个很讽刺的情况:网关把所有未登录的请求都拦住了,但登录之后你能调所有接口。

3. 习惯"记录谁改的",不习惯"校验是谁的"

代码里到处是 SecurityUtils.getUsername() 用来记操作人,但 getUserId() 用来校验数据归属的,少得可怜。

好像大家默认了"只要记录了是谁改的,就没问题"------但越权的问题恰恰是:他改了不该他改的东西,你只记录下来有什么用?

我是怎么扫出来的

方法很笨,一条 grep 命令的事:

bash 复制代码
# 找「无权限注解的映射方法」:映射方法前 6 行内没有权限/匿名/内部注解的,全部列出
grep -rn -B6 "@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping" \
  --include="*Controller.java" . \
  | grep -v "RequiresPermissions\|RequiresRoles\|Anonymous\|InnerAuth" \
  | grep "@.*Mapping"

逻辑很简单:每个 @XxxMapping 方法,往前看 6 行,如果没有任何权限相关的注解,就列出来。

扫出来多少个呢?87 个。

也就是说,整个项目里有 87 个接口是"只要登录就能调"的。

哪些最危险

不是所有漏注解的接口都一样危险,我按风险从高到低排了个序:

🔴 最高危:写操作无权限注解

  • 生成 / 重算 / 审批 / 删除类的 POST/PUT/DELETE 接口
  • 普通低权限账号一点击就能触发全量计算
  • 这种必须最先补

🟡 中危:详情 / 修改 / 删除接口

  • 直接拿前端传的 ID 查库/改库/删库
  • 没有校验数据归属
  • 改个 URL 参数就能越权

🟢 低危:列表查询接口

  • 虽然可能看到全部数据,但至少没有写操作
  • 补上数据权限注解就行

怎么修

分三层,从急到缓:

第一层:先把功能权限补上

所有没有 @RequiresPermissions 的接口,先按模块补上功能权限。87 个接口,按优先级从高到低补,写操作最先。

这一步能挡住"低权限账号调高级功能"的问题。

第二层:资源 ID 从登录态反查

这条最关键------不要直接信任前端传进来的资源 ID。

举个例子,老师看自己班上的学生,正确的逻辑是:

java 复制代码
// 错误:直接用前端传的 studentId
Student student = studentService.selectStudentById(studentId);

// 正确:先拿登录用户的学校/班级范围,再查学生
Long teacherId = SecurityUtils.getUserId();
List<Long> classIds = teacherService.getClassIdsByTeacher(teacherId);
Student student = studentService.selectStudentByIdAndClassIds(studentId, classIds);

⚠️ 这里有个容易踩的坑:getUserId() 返回的是登录用户 ID ,不是业务资源 ID(学生 ID / 班级 ID)。直接 studentId = getUserId() 会引入 ID 混用的 bug,必须先反查绑定关系。

第三层:列表查询补数据权限

列表类的接口,统一加 @DataScope 注解或者在 SQL 里拼归属条件。这一步量最大,但做完之后整个数据层的权限就闭环了。

最后说两句

做这次审计之前,我也以为"网关有认证、接口有权限注解"就差不多安全了。

扫完之后才发现,认证和鉴权是两层,任何一层漏了都是洞。

  • 认证 = 你是谁(网关层)
  • 功能权限 = 你能不能调这个接口(服务层注解)
  • 数据权限 = 你能看哪些行的数据(服务层注解 + SQL)

大多数团队都做到了前两层,第三层大面积遗漏。而越权事故,十有八九出在第三层。


自查清单,你的项目也可以跑一下:

  • 所有 Controller 接口都有 @RequiresPermissions 吗?
  • 详情 / 修改 / 删除接口校验数据归属了吗?
  • 写操作(生成 / 重算 / 审批 / 删除)都有权限注解吗?
  • 资源 ID 是从登录态反查,还是直接信任前端入参?
  • 列表查询带数据权限过滤吗?
相关推荐
小羊没烦恼!1 小时前
分布式事务 | 使用DTM 的Saga 模式
java·linux·运维·数据库·wpf
JPower_mr.g1 小时前
SmartCall v1.0.9 发布:智能体知识库上线,让 AI 客服先查资料再回答
java·ide·人工智能·macos·架构·开源·xcode
SL_staff1 小时前
破除执行黑箱:用目标地图、资源热力图与健康度看板构建可下钻的执行事实链
java·开源·全栈
骑着蜗牛撵大象3272 小时前
让 Codex 值夜班:把告警排查与巡检接成可回滚的自动化链路
android·java·运维·ai·自动化·codex·智能体
木头科技2 小时前
【AI 工程化第十八篇】Spring AI 语音 Agent 实战:实时转写、WebSocket、流式 TTS 与打断机制
java·人工智能·安全·spring·性能优化
cfm_29142 小时前
SpringPrototype作用域
java
大风起兮云飞扬丶2 小时前
Java——枚举
java·开发语言
ReStock活库3 小时前
Zephyr异步SPI调用返回后,为什么不能立即复用发送缓冲区?
java·前端·网络
微微%3 小时前
从零搭建一个java开发环境
java·idea2023·jdk17