记一次代码审计之ruoyi(二)接口鉴权与部署加固

admin 2026-09-14 04:45:14 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入分析若依微服务版RuoYi-Cloudv3.6.8存在的接口鉴权缺失与部署暴露面问题,发现文件上传删除接口未鉴权导致任意文件操作,多处配置与权限接口越权读取泄露敏感信息,且actuator监控端点与Swagger文档未授权暴露。建议修复时增加@InnerAuth注解、按文件归属校验删除、限制端口外放并收紧actuator暴露范围,同时通过批量排查漏标注解提升整体安全性。 综合评分: 92 文章分类: 代码审计,漏洞分析,WEB安全,安全建设,解决方案


记一次代码审计之ruoyi(二)接口鉴权与部署加固

Khan安全团队

2026年9月11日 09:42 广东

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

记一次代码审计之 RuoYi-Cloud(二):接口鉴权缺失与部署加固

系列文章第二篇,聚焦接口层鉴权缺失与部署暴露面:从”批量排查漏标权限注解的接口”这一高效审计手法入手,逐一还原文件服务未鉴权、越权读取、actuator/swagger 暴露、运行时竞态与纵深防御缺口。

审计目标:若依微服务版 RuoYi-Cloud v3.6.8

  1. 引言

第一篇讲的是”信任了不该信任的输入”。本篇讲的则是另一类根因:该设防的地方没设防——大量接口只依赖”网关要求登录”这一层兜底,业务层没有自己的权限校验;以及该隐藏的东西没藏住——监控端点、接口文档、内部端口直接暴露。

本篇覆盖的问题:

| 漏洞 | 严重性 | 一句话 | | — | — | — | | 文件上传/删除接口未鉴权 | 🔴 高 | 未授权上传 + 任意登录用户删除他人文件 | | 系统参数配置越权读取 | 🟠 中 | 遍历读取全部 sys_config(含黑名单/初始密码) | | 角色菜单权限越权读取 | 🟡 中低 | 枚举角色查看任意角色的权限分配 | | 公告详情越权读取 | 🟡 中低 | 遍历读取任意公告全文 | | Gen 表结构越权访问 | 🟡 低 | 任意登录用户读取代码生成表结构 | | Monitor actuator 未授权 | 🟠 中 | /actuator/env、/heapdump 等未认证可达 | | Swagger 接口文档暴露 | 🟡 中低 | 未登录获取全量接口定义 | | ValidateCodeFilter 竞态 | 🟠 中 | 验证码校验不可靠,登录链路可用性缺陷 | | 定时任务 DB 篡改 → RCE | 🟠 中 | 执行路径无二次白名单校验 |

  1. 审计方法:权限注解缺失的批量排查

RuoYi 系项目的权限模型是注解驱动的:Controller 方法上标注 @RequiresPermissions(“system:xxx:yyy”),由 PreAuthorizeAspect 切面统一鉴权。因此”漏标注解”就是直接漏权限。批量排查非常高效:

# 找出所有 Controller 方法,逐一核对上方是否有权限注解grep&nbsp;-rn&nbsp;"public \(AjaxResult\|TableDataInfo\|R<\|void\)"&nbsp;--include="*Controller.java"&nbsp;ruoyi-modules/ |&nbsp;grep&nbsp;-v&nbsp;"RequiresPermissions"

更可靠的验证手法是”对照请求”:同一低权限账号,访问疑似越权接口返回 200,访问该模块”需要权限”的列表接口返回 403——200 说明不是账号有权限,而是注解缺失。以 /system/config 为例:

# 请求A:疑似越权GET&nbsp;/system/config/1&nbsp;HTTP/1.1Authorization: Bearer <低权限token># → 200 返回配置
# 请求B:对照(有权限注解)GET&nbsp;/system/config/list HTTP/1.1Authorization: Bearer <低权限token># → 403 "没有访问权限,请联系管理员授权"

A 能读、B 被拒,即可坐实”注解缺失”而非”账号有权限”。整套 PoC 里都用这个思路,防误报。

  1. 高危:文件上传/删除接口完全未鉴权

3.1 漏洞点①:Controller 无任何鉴权注解

ruoyi-modules/ruoyi-file/src/main/java/com/ruoyi/file/controller/SysFileController.java:32-71

@PostMapping("upload")public&nbsp;R<SysFile>&nbsp;upload(MultipartFile&nbsp;file) { ... } &nbsp;&nbsp;// 无 @RequiresPermissions / @InnerAuth
@DeleteMapping("delete")public&nbsp;R<Boolean>&nbsp;delete(String&nbsp;fileUrl) {&nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!FileUtils.validateFilePath(fileUrl)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;new&nbsp;Exception("资源文件({})非法,不允许删除。");&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; sysFileService.deleteFile(fileUrl); &nbsp;&nbsp;// 直接删除&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;R.ok();&nbsp; &nbsp; } ...}

检查 file 服务的 pom.xml:它甚至没有引入 ruoyi-common-security,意味着服务自身连拦截器都没有,全靠网关兜底。而网关对 /file/** 只要求”有登录 token”,任意低权限用户即可调用。

3.2 漏洞点②:删除只做弱校验

ruoyi-modules/ruoyi-file/src/main/java/com/ruoyi/file/service/LocalSysFileServiceImpl.java:60-64

@Overridepublic&nbsp;void&nbsp;deleteFile(String fileUrl)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp;&nbsp;String&nbsp;localFile&nbsp;=&nbsp;StringUtils.substringAfter(fileUrl, localFilePrefix);&nbsp; &nbsp; FileUtils.deleteFile(localFilePath + localFile); &nbsp;&nbsp;// 拼接后直接删}

FileUtils.validateFilePath 只拦 .. + 限制扩展名(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION:png/jpg/txt/zip/doc/pdf 等)。也就是上传目录内任何符合扩展名的文件都能被删,包括他人上传的。

3.3 漏洞点③:docker-compose 把端口暴露到宿主机

docker/docker-compose.yml 将 file 服务 9300 端口映射到宿主机 → 直连 http://:9300/upload 时连 token 都不要,完全未授权。

3.4 攻击链与验证

flowchart LR&nbsp; &nbsp; A[攻击者]&nbsp;-->|POST /upload 无认证| B[file服务 9300]&nbsp; &nbsp; B&nbsp;--> C[(磁盘写入任意文件)]&nbsp; &nbsp; A&nbsp;-->|DELETE /delete?fileUrl=目标文件| B&nbsp; &nbsp; B&nbsp;--> D[(删除他人上传文件)]&nbsp; &nbsp; style B fill:#ffcdd2,color:#b71c1c
# 未授权上传(直连 file 服务)POST /upload HTTP/1.1Host:&nbsp;127.0.0.1:9300Content-Type: multipart/form-data; boundary=----x------xContent-Disposition: form-data; name="file"; filename="poc.txt"Content-Type: text/plain
poc------x--
# 未授权删除DELETE /delete?fileUrl=http%3A%2F%2F127.0.0.1%3A8080%2Fprofile%2F...%2Fpoc.txt HTTP/1.1Host:&nbsp;127.0.0.1:9300

影响:存储耗尽、存放违规内容、删除他人文件造成数据丢失(DoS)。上传扩展名白名单不含脚本类型,未直接 RCE——但如果部署方自定义了允许 html/jsp 等,风险立即升级为存储型 XSS/webshell。

3.5 修复建议

  • upload/delete 增加 @InnerAuth(需先给 file 模块引入 ruoyi-common-security),仅允许内部 Feign 调用;

  • 删除改为按文件归属(ownerId)校验;

  • 生产环境禁止 file 服务端口外放。

  1. 权限注解缺失系列:越权读取

用第 2 节的批量手法,命中 4 个越权读取点。

4.1 系统参数配置越权读取(中)

ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysConfigController.java:63-76

@GetMapping(value =&nbsp;"/{configId}") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 无 @RequiresPermissionspublic&nbsp;AjaxResult&nbsp;getInfo(@PathVariable&nbsp;Long&nbsp;configId) {&nbsp; &nbsp;&nbsp;return&nbsp;success(configService.selectConfigById(configId));}
@GetMapping(value =&nbsp;"/configKey/{configKey}") &nbsp;// 无 @RequiresPermissionspublic&nbsp;AjaxResult&nbsp;getConfigKey(@PathVariable&nbsp;String&nbsp;configKey) {&nbsp; &nbsp;&nbsp;return&nbsp;success(configService.selectConfigByKey(configKey));}

影响:任意登录用户可遍历 configId 读取全部 sys_config——sys.login.blackIPList(登录黑名单 IP,可用于针对性绕过)、sys.user.initPassword(初始密码)、密码策略等内部配置全部泄露。

4.2 角色菜单权限越权读取(中低)

ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysMenuController.java:74-83

@GetMapping(value =&nbsp;"/roleMenuTreeselect/{roleId}") &nbsp;&nbsp;// 无 @RequiresPermissionspublic&nbsp;AjaxResult&nbsp;roleMenuTreeselect(@PathVariable("roleId")&nbsp;Long&nbsp;roleId) {&nbsp; &nbsp; ...&nbsp; &nbsp; ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); &nbsp;// 任意角色的菜单权限&nbsp; &nbsp; ...}

影响:枚举 roleId 即可还原整个 RBAC 权限模型(哪些角色拥有哪些菜单),便于横向/垂直探测。

4.3 公告详情越权读取(中低)

ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysNoticeController.java:57-61

@GetMapping(value =&nbsp;"/{noticeId}")&nbsp; &nbsp;&nbsp;// 无 @RequiresPermissionspublic&nbsp;AjaxResult getInfo(@PathVariable&nbsp;Long&nbsp;noticeId) {&nbsp; &nbsp;&nbsp;return&nbsp;success(noticeService.selectNoticeById(noticeId));}

影响:遍历 noticeId 读取任意公告全文。公告往往包含内部通知、链接、账号信息。

4.4 代码生成表结构越权访问(低)

ruoyi-modules/ruoyi-gen/src/main/java/com/ruoyi/gen/controller/GenController.java:92-100

@GetMapping(value =&nbsp;"/column/{tableId}") &nbsp;&nbsp;// 无 @RequiresPermissions(同文件其他接口都有)public&nbsp;TableDataInfo&nbsp;columnList(Long&nbsp;tableId) {&nbsp; &nbsp; ...}

影响:任意登录用户枚举 tableId 获取已导入表的列名/类型/注释,泄露数据库表结构。

4.5 汇总

| 接口 | 泄露内容 | 修复 | | — | — | — | | GET /system/config/{id}、/configKey/{key} | 全部系统配置 | 加 system:config:query | | GET /system/menu/roleMenuTreeselect/{roleId} | 角色权限分配 | 加 system:menu:query | | GET /system/notice/{noticeId} | 公告全文 | 加 system:notice:query | | GET /gen/column/{tableId} | 表结构元数据 | 加 tool:gen:query |

共性根因:开发时”顺手没标注解”,又依赖网关登录态兜底,权限缺口被掩盖。批量排查 + 对照请求验证是这类问题最高效的审计方式。

  1. 部署暴露面:监控与文档

5.1 Monitor actuator 未授权(中)

ruoyi-visual/ruoyi-monitor/src/main/java/com/ruoyi/modules/monitor/config/WebSecurityConfigurer.java:40-44

.authorizeHttpRequests(&nbsp; &nbsp; (authorize) -> authorize&nbsp; &nbsp; &nbsp; &nbsp; .requestMatchers(adminContextPath +&nbsp;"/assets/**",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; adminContextPath +&nbsp;"/login",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; adminContextPath +&nbsp;"/actuator/**", &nbsp; &nbsp;// ← 未授权放行&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; adminContextPath +&nbsp;"/instances/**") &nbsp;&nbsp;// ← 未授权放行&nbsp; &nbsp; &nbsp; &nbsp; .permitAll()&nbsp; &nbsp; &nbsp; &nbsp; .anyRequest().authenticated())

影响(取决于 nacos 中 management.endpoints.web.exposure.include 的暴露范围):/actuator/env 泄露环境变量(可能含 DB/Redis 凭据)、/actuator/heapdump 转储堆内存(含内存中的密钥/Token)、/instances/** 泄露全部注册实例元数据。

GET&nbsp;/actuator/env HTTP/1.1Host:&nbsp;127.0.0.1:9100# 未认证返回 200 → 高危信息泄露

修复:仅放行 /actuator/health、/actuator/info;exposure.include 只保留 health,info;端口不外放。

5.2 Swagger 接口文档暴露(中低)

ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/SpringDocConfig.java:54-81

private&nbsp;final&nbsp;static&nbsp;String[]&nbsp;EXCLUDE_ROUTES&nbsp;=&nbsp;new&nbsp;String[] {&nbsp;"ruoyi-gateway",&nbsp;"ruoyi-auth",&nbsp;"ruoyi-file",&nbsp;"ruoyi-monitor"&nbsp;};...swaggerUrl.setUrl(String.format("/%s/v3/api-docs", instance.getServiceId()));

网关把 system/job/gen 的 /v3/api-docs 聚合进 Swagger UI。配合官方默认 nacos security.ignore.whites 放行 /v3/api-docs 与 /swagger-ui/**:

GET&nbsp;/v3/api-docs HTTP/1.1Host:&nbsp;127.0.0.1:8080# 未登录返回全量接口定义(路径/参数/模型)

影响:攻击面全量暴露,大幅降低渗透成本。修复:网关默认不放行 swagger 路径,或加登录认证。

  1. 运行时缺陷与纵深防御

6.1 ValidateCodeFilter 异步读 body 竞态(中)

ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java:67-79

private&nbsp;String&nbsp;resolveBodyFromRequest(ServerHttpRequest&nbsp;serverHttpRequest) {&nbsp; &nbsp;&nbsp;Flux<DataBuffer> body = serverHttpRequest.getBody();&nbsp; &nbsp;&nbsp;AtomicReference<String> bodyRef =&nbsp;new&nbsp;AtomicReference<>();&nbsp; &nbsp; body.subscribe(buffer -> { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ① 异步订阅&nbsp; &nbsp; &nbsp; &nbsp; ...&nbsp; &nbsp; &nbsp; &nbsp; bodyRef.set(charBuffer.toString());&nbsp; &nbsp; });&nbsp; &nbsp;&nbsp;return&nbsp;bodyRef.get(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// ② 立即取值 → 竞态!}

subscribe 是异步的,bodyRef.get() 大概率拿到 null:JSON.parseObject(null) → NPE → 登录被拒;若时序恰好成功,body 已被消费且未缓存回放,下游 auth 服务收到空 body → “用户/密码必须填写”。开启验证码后登录链路不可靠(表现为 500/登录失败为主),是认证链路的可用性缺陷。

修复:改用 DataBufferUtils.join(body).block() 阻塞读取,并用 ServerHttpRequestDecorator 缓存 body 回放下游。

6.2 定时任务 DB 篡改 → 任意方法反射 RCE(中)

ruoyi-modules/ruoyi-job/src/main/java/com/ruoyi/job/util/JobInvokeUtil.java:23-40

public&nbsp;static&nbsp;void&nbsp;invokeMethod(SysJob sysJob)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp;&nbsp;String&nbsp;invokeTarget&nbsp;=&nbsp;sysJob.getInvokeTarget();&nbsp; &nbsp;&nbsp;String&nbsp;beanName&nbsp;=&nbsp;getBeanName(invokeTarget);&nbsp; &nbsp; ...&nbsp; &nbsp;&nbsp;if&nbsp;(!isValidClassName(beanName)) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;bean&nbsp;=&nbsp;SpringUtils.getBean(beanName); &nbsp; &nbsp; &nbsp; &nbsp;// 任意 Spring bean&nbsp; &nbsp; &nbsp; &nbsp; invokeMethod(bean, methodName, methodParams); &nbsp; &nbsp; &nbsp; &nbsp;// 反射调用&nbsp; &nbsp; }&nbsp;else&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;bean&nbsp;=&nbsp;Class.forName(beanName).getDeclaredConstructor().newInstance(); &nbsp;// 任意类实例化&nbsp; &nbsp; &nbsp; &nbsp; invokeMethod(bean, methodName, methodParams);&nbsp; &nbsp; }}

白名单(ScheduleUtils.whiteList 限定 com.ruoyi.job.task)和黑名单(javax.naming.InitialContext、org.yaml.snakeyaml 等)只在 SysJobController 的 add/edit 入口校验,而 Quartz 执行路径 QuartzJobExecution → JobInvokeUtil.invokeMethod不做二次校验。

风险模型:正常 API 权限下 RCE 被阻止(白名单生效);但一旦 sys_job 表被篡改(DB 泄露、SQL 注入、高权限管理员被攻破),任务触发时即绕过入口白名单,反射调用任意类方法 → RCE。

修复:JobInvokeUtil.invokeMethod 运行时二次调用 ScheduleUtils.whiteList;invoke_target 建议改为受控枚举。

  1. 已核查无风险项(防误报清单)

审计里”排雷”和”挖洞”同样重要,以下点均已核查无问题,避免误报:

| 检查项 | 结论 | 依据 | | — | — | — | | Mapper XML SQL 注入 | ✅ 安全 | 全部参数化 #{};${params.dataScope} 由服务端角色配置拼接,不可注入 | | 排序字段注入 | ✅ 安全 | SqlUtil.escapeOrderBySql 正则 [a-zA-Z0-9_ ,.]+ 白名单 + 500 长度限制 | | Fastjson 反序列化 | ✅ 可控 | fastjson2 2.0.64 + JSONReader.autoTypeFilter 白名单仅 com.ruoyi | | 定时任务入口 RCE | ✅ 被阻止 | 入口白名单 + rmi/ldap/http 黑名单(详见 6.2 的纵深缺口) | | Excel 公式注入 | ✅ 已修复 | ExcelUtil.java:966-976 对 =/+/-/@ 前缀加 \t 防护 | | druid 监控页 | ✅ 未启用 | 无 StatViewServlet 配置 | | 在线用户强退 | ✅ 有鉴权 | monitor:online:forceLogout | | 数据权限越权 | ✅ 有隔离 | DataScopeAspect 从 Redis LoginUser 取角色拼接,且入口 clearDataScope 先清空防注入 |

  1. 综合修复建议

P0(立刻)

  1. file 服务 upload/delete 加 @InnerAuth + 引入 ruoyi-common-security,删除改为按 owner 校验;

  2. 补齐 SysConfigController/SysMenuController/SysNoticeController/GenController 缺失的 @RequiresPermissions。

P1(尽快)

  1. Monitor 仅放行 /actuator/health|info,exposure.include 收紧,端口不外放;

  2. 网关默认不放行 /v3/api-docs、/swagger-ui/**;

5. JobInvokeUtil 执行路径加二次白名单校验;

  1. 修复 ValidateCodeFilter 竞态(阻塞读取 + body 回放)。

P2(加固)

  1. 网关覆盖 X-Forwarded-For;登录文案统一防枚举;密码锁定计数绑定 IP;内部服务端口不映射公网。

  2. 总结

本篇文章覆盖的 9 个问题,归为两类根因:

该设防的地方没设防: &nbsp;文件接口无鉴权、4个越权读取接口、actuator/swagger 暴露纵深防御不足: &nbsp; &nbsp; &nbsp; &nbsp;验证码竞态、定时任务运行期无二次校验

*系列:《记一次代码审计之 RuoYi-Cloud(一)认证授权与信任边界》 / (二)接口鉴权缺失与部署加固。本文仅用于授权范围内的安全研究与学习。*


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Khan安全团队 《记一次代码审计之ruoyi(二)接口鉴权与部署加固》

评论:0   参与:  0