ruoyi-ai-安全审计报告

16次阅读
没有评论

RuoYi-AI 授权安全审计报告

审计对象: /ruoyi-ai(RuoYi AI 后端)
审计日期: 2026-08-14
审计方式: 静态代码审计(只读,未修改任何文件);并行语义审查 + 逐条人工源码核验
技术栈: Spring Boot 3.5.8 / Java 17 / Langchain4j 1.17.2 / MyBatis-Plus / Sa-Token + JWT
范围: ruoyi-admin、ruoyi-common(27 子模块)、ruoyi-modules(chat / aiflow / workflow / system / generator)、ruoyi-extend(monitor-admin / snailjob-server)


一、执行摘要

本次审计共确认 2 个严重 (Critical)、8 个高危 (High)、6 个中危 (Medium)、3 个低危 (Low) 问题。

其中最严重的两个问题集中在 /coding/** 未授权入口上——该路径被配置在 security.excludes 白名单中(完全绕过登录认证),并对外暴露了一个可直接执行任意命令、读写任意文件的接口。配合 python -c 绕过命令白名单,攻击者无需任何账号即可在应用服务器上实现 未认证远程命令执行 (Unauthenticated RCE) 与 任意文件读写。

严重程度 数量 关键问题
🔴 Critical 2 未认证 RCE + 任意文件读写(/coding/**)
🟠 High 8 任意 DB 读、OSS 上传任意文件写入、工作流 SSRF、LLM API Key 泄露、会话越权 (IDOR)、硬编码凭据、未授权工作流执行
🟡 Medium 6 Redis 反序列化、MCP 命令执行/SSRF、存储型 XSS 载体、匿名 WebSocket、硬编码密钥
🟢 Low 3 日志注入、SQL 拼接异味、inSql 调用

所有关键发现均通过直接读取源码二次核验,不存在「疑似未证实」项。文内敏感密钥类值已脱敏。


二、严重 (Critical)

C-1 未认证远程命令执行 —— /coding/command

  • 位置: ruoyi-modules/ruoyi-chat/.../controller/coding/CodingController.java → CodingWorkspaceService.execute() → ExecuteCommandTool.executeCommand()
  • 认证状态: application.yml 的 security.excludes 包含 /coding/**,完全绕过登录认证
  • 触发链:
    1. POST /coding/command 直接接收 {workspacePath, command} 并执行;
    2. ExecuteCommandTool 虽有命令白名单(npm/pnpm/yarn/git/mvn/...)和元字符黑名单(&|;\$<>\n\r),但 python/python3` 本身在白名单内;
    3. 绕过方式(无需任何被禁字符):python -c "__import__('os').system('id')" —— -c 字符串中不含被黑名单拦截的字符即可任意命令执行;
    4. ProcessBuilder(cmdList) 以应用进程身份执行,可读取 /etc/passwd、写 crontab / 反弹 shell / 读取应用配置中所有密钥。
  • 利用条件: 无(匿名即可)
  • 影响: 服务器完全失陷——RCE、提权、横向移动
  • 修复建议:
    1. 立即将 /coding/** 从 security.excludes 移除,必须登录;
    2. 给 CodingController 增加 @SaCheckPermission 鉴权,并按用户隔离工作区;
    3. ExecuteCommandTool 移除 python/python3 白名单或改用严格 argv 模板(禁止 -c/-m),对不可信命令一律拒绝。

C-2 未认证任意文件读写 + 代理 RCE —— /coding/chat

  • 位置: ruoyi-modules/ruoyi-chat/.../service/coding/impl/CodingServiceImpl.java:94-112,145-150
  • 触发链:
    1. POST /coding/chat 携带 workspacePath 与 prompt(匿名可访问);
    2. resolveWorkspace(bo.getWorkspacePath()) 对路径完全无限制:传 workspacePath="/" 即返回根目录 Paths.get("/");
    3. 以 root=/ 构建 ReadFileTool / WriteFileTool / EditFileTool / DeleteFileTool / ExecuteCommandTool 五个工具并注入 Langchain4j agent(CodingServiceImpl.java:98-109);
    4. 通过自然语言指示 agent 调用工具(如「读取 /etc/shadow」「写 /root/.ssh/authorized_keys」「执行 id」),即获得任意文件读写 + 命令执行能力。
  • 对比: 同包的 CodingWorkspaceService.resolveRoot()(CodingWorkspaceService.java:74-82)做了工作区强校验,但 CodingServiceImpl.resolveWorkspace() 漏掉该校验,两处实现不一致。
  • 利用条件: 无(匿名即可)
  • 影响: 与 C-1 相同,服务器完全失陷
  • 修复建议:
    1. /coding/** 移除出白名单并加权限校验(同 C-1);
    2. resolveWorkspace() 复用 CodingWorkspaceService.resolveRoot() 的强校验逻辑,拒绝任意绝对路径;
    3. 为 agent 工具增加沙箱/容器隔离(如仅允许操作预置工作区,禁止 .. 与绝对路径)。

三、高危 (High)

H-1 SQL 注入 / 任意数据库读取 —— ExecuteSqlQueryTool

  • 位置: ruoyi-modules/ruoyi-chat/.../agent/tool/ExecuteSqlQueryTool.java:127-136
  • 问题: 仅以白名单 SELECT 开头 + 正则 (?:FROM|JOIN)\s+"?([A-Z0-9_]+)"? 提取表名并做表名白名单校验。但逗号形式 FROM sys_user, secret_table 可绕过该正则,直接命中任意表;且该工具使用主 DataSource(SpringUtils.getBean(DataSource.class),agent 隔离数据源被注释),即 LLM 对话可让 agent 读取业务库全量数据。
  • 限制: 仅 SELECT、1000 行、8 列输出——但已足以拖取任意表内容。
  • 修复建议: 使用 JSqlParser 解析 AST 校验表名;查询走独立只读数据源/账号;禁用直连主库。

H-2 OSS 上传越权 + 路径穿越任意文件写入 —— /resource/oss/fileUpload

  • 位置:
    • ruoyi-modules/ruoyi-system/.../controller/system/SysOssController.java:89-96 —— fileUpload 无 @SaCheckPermission(对比同文件 /upload 有 system:oss:upload 权限注解)
    • ruoyi-modules/ruoyi-system/.../service/impl/SysOssServiceImpl.java:270-303 —— fileUpload() 中 originalName = file.getOriginalFilename() 未经任何 .. 清洗,Path targetPath = uploadDir.resolve(originalName); file.transferTo(pathFile) 直接以客户端文件名落盘。
  • 利用条件: 任意已登录的低权限用户(无需 OSS 权限)
  • 触发: 上传文件名 ../../../../tmp/pwn.jsp 或覆盖任意可执行/配置文件 → 任意文件写入。
  • 修复建议: 为 fileUpload 补 @SaCheckPermission("system:oss:upload");对原始文件名做 Path.normalize() 后校验必须位于 uploadDir 内;统一用服务端生成文件名(白名单后缀 + 随机名)。

H-3 工作流节点 SSRF + 未授权工作流执行

  • 位置:
    • ruoyi-modules/ruoyi-aiflow/.../node/httpRequest/HttpRequestNode.java:43,403,434 —— renderTemplate(config.getUrl(), inputs) 后 restTemplate.exchange(url, ...) 请求目标 URL,URL 完全由工作流输入控制,无 SSRF 防护(无内网地址/环回地址黑名单);
    • ruoyi-modules/ruoyi-aiflow/.../workflow/controller/WorkflowController.java —— 全部端点无 @SaCheckPermission;
    • application.yml security.excludes 含 /workflow/run → 匿名可触发任意工作流。
  • 利用: 匿名构造工作流执行 POST /workflow/run,让 HttpRequestNode 探测内网(如 http://127.0.0.1:3306 / 云元数据服务 169.254.169.254),或让 SqlNode 读取业务数据。
  • 修复建议: 工作流端点补权限注解;/workflow/run 移出白名单(如需匿名则单独做 token 校验);HttpRequestNode 增加内网 IP / 元数据地址 / 环回地址黑名单,并对 URL scheme 做白名单(http/https)。

H-4 LLM API Key 批量泄露 —— /system/model/modelList

  • 位置: ruoyi-modules/ruoyi-chat/.../ChatModelController.java:57-63 —— GET /system/model/modelList 无权限注解;ruoyi-common/.../vo/chat/ChatModelVo.java:79-80 的 apiKey 字段无脱敏。
  • 利用: 任意已登录用户访问该接口即拿到全量模型配置的全部 API Key(含所有供应商)。
  • 修复建议: 接口补 @SaCheckPermission("system:model:list");VO 输出侧对 apiKey 脱敏(******** 前缀 + 尾 4 位)。

H-5 聊天会话越权 (IDOR) —— /chat/send

  • 位置:
    • ruoyi-modules/ruoyi-chat/.../controller/chat/ChatController.java:30-33 —— POST /chat/send 直接采用客户端提交的 chatRequest.getSessionId();
    • ruoyi-modules/ruoyi-chat/.../service/chat/impl/ChatServiceFacade.java:206,215,650-686 —— buildContextMessages() → createChatMemory(sessionId) 加载历史;
    • ruoyi-modules/ruoyi-chat/.../service/chat/impl/ChatMessageServiceImpl.java:76-87,149-166,178-186 —— getMessagesBySessionId / deleteBySessionId 的查询条件仅 sessionId,不校验 userId 归属。
  • 触发: 用户 A 以用户 B 的 sessionId 调用 /chat/send,服务端将 B 的全部聊天历史注入上下文(可通过提示词让模型复述以读出),并可向 B 的会话追加消息、deleteBySessionId 清空 B 的历史。session_id 为自增 Long,可遍历枚举。
  • 修复建议: 所有会话读写接口以 userId + sessionId 复合校验归属;创建/访问会话前验证该会话属于当前用户。

H-6 数据库 / 监控台硬编码凭据

  • 位置: ruoyi-admin/src/main/resources/application-dev.yml、application-prod.yml
    • 第 62-64 行:MySQL username: root, password: root(生产 profile 同样硬编码 root/root);
    • 第 12-13 行:监控台 MONITOR_PASSWORD:123456 默认口令。
  • 风险: 若数据库端口或监控台端口对外暴露,等于默认口令直达数据库。
  • 修复建议: 改用环境变量注入并强制覆盖默认值;部署时校验禁止空口令/root 口令;数据库与监控台端口不对外。

H-7 其它未授权越权操作(工作流)

  • 位置: WorkflowController(add/update/del/enable 等)无权限注解;WorkflowService.softDelete 无归属校验。
  • 影响: 任意登录用户可新增/修改/删除/启停任何工作流。
  • 修复建议: 补 @SaCheckPermission;删除/修改前校验创建人归属。

H-8 默认管理员账号

  • 位置: 初始化 SQL(admin/admin123)。
  • 风险: 未强制改密的默认管理员可直接登录后台。
  • 修复建议: 首次登录强制改密;审计账号体系内已存在的默认口令。

四、中危 (Medium)

M-1 Redis 反序列化风险(Jackson 默认类型开启)

  • 位置: ruoyi-common/.../redis/config/RedisConfig.java:55 —— activateDefaultTyping(LaissezFaireSubTypeValidator.instance, NON_FINAL)。
  • 风险: 若攻击者可写入 Redis(如 SSRF 到 Redis 或内网可达),结合反序列化 gadget 可实现 RCE;LaissezFaire + NON_FINAL 覆盖面最大。
  • 修复建议: 改用白名单 BasicPolymorphicTypeValidator(仅放行项目内已知类型),或关闭默认类型改用显式序列化。

M-2 MCP 工具:LOCAL 命令执行 / 市场 SSRF

  • 位置: ruoyi-modules/ruoyi-chat/.../mcp/(LOCAL 工具)、MCP 市场注册逻辑。
  • 风险: LOCAL 类型 MCP 工具以管理员配置的任意命令/文件能力运行;MCP 市场拉取远端描述符存在 SSRF 面。
  • 修复建议: LOCAL 工具仅允许管理员配置并加操作审计;市场拉取走服务端白名单 + 内网地址黑名单。

M-3 存储型 XSS 载体(OSS 静态资源)

  • 位置: SysOssServiceImpl.upload() 未限制文件类型,对象存储若以静态域名直出,上传的 HTML/SVG 可在受害用户同域下执行脚本。
  • 修复建议: 上传时校验 MIME/后缀白名单;静态域名设置 Content-Security-Policy / X-Content-Type-Options: nosniff;与主站分离域名。

M-4 匿名 WebSocket /chat/ws

  • 位置: ruoyi-chat 的 WebSocket 端点(未纳入 Sa-Token 认证)。
  • 风险: 匿名可建立连接并订阅/推送事件(与 H-5 叠加可进一步探测会话数据)。
  • 修复建议: WebSocket 握手时校验 token,并按用户隔离会话订阅。

M-5 多个硬编码密钥

  • 位置: JWT/加密相关密钥以固定值写在配置/代码中。
  • 修复建议: 统一迁移到环境变量/密钥管理服务,生产禁用默认密钥。

M-6 上传文件类型未校验(千问百炼 fileUpload)

  • 位置: SysOssServiceImpl.fileUpload() 未做扩展名/内容校验,任意类型可上传。
  • 修复建议: 与 H-2 一并修复,白名单后缀 + 魔数校验。

五、低危 (Low)

L-1 日志注入

  • 用户可控内容(如 command、workspacePath)进入日志,存在日志伪造/注入面。建议对换行符与敏感字段清洗。

L-2 DataBaseHelper.findInSet / SysPostMapper inSql 代码气味

  • MyBatis inSql/拼接式查询在少数场景出现,当前未见直接注入点,但属于风险放大面,建议统一走参数化条件构造。

L-3 外部命令/路径依赖(ffmpeg 等)

  • 依赖系统命令路径的调用(如 ffmpeg)未做路径白名单,存在环境差异与潜在注入面。

六、修复优先级建议(落地顺序)

  1. 本周内(阻断)
    • 将 /coding/**、/workflow/run 移出 security.excludes,补权限注解与鉴权;
    • 修复 CodingServiceImpl.resolveWorkspace() 工作区强校验(复用 resolveRoot);
    • ExecuteCommandTool 去掉 python/python3 白名单或禁止 -c/-m 参数。
  2. 两周内(止血)
    • OSS fileUpload 补权限 + 文件名 normalize 校验;
    • 模型 modelList 补权限 + apiKey 脱敏;
    • 会话读写做 userId + sessionId 归属校验;
    • 工作流 HttpRequestNode 加 SSRF 防护;
    • 清理硬编码 root/root、默认 admin、监控台默认口令。
  3. 一个月内(加固)
    • Redis Jackson 默认类型改为白名单验证器;
    • MCP 工具、WebSocket、日志与上传类型全面加固;
    • 密钥统一迁移到环境变量/密钥管理。

七、附录:审计方法与覆盖

  • 方法: 5 路并行语义审查(SQL 注入 / 反序列化+RCE / SSRF+文件上传下载 / 硬编码凭据+认证绕过 / XSS+其它注入),每个高价值发现均回读源码逐行确认(含行号)。
  • 未发现: 项目已从依赖树中排除 fastjson;日志框架为 Logback(非 Log4j2),未见对应已知 CVE 链。
  • 声明: 本报告为授权防御性审计,所有结论基于静态代码分析,未进行实际漏洞验证(未发起任何攻击流量);利用条件均基于代码路径推导。
正文完
 0