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/**,完全绕过登录认证 - 触发链:
POST /coding/command直接接收{workspacePath, command}并执行;ExecuteCommandTool虽有命令白名单(npm/pnpm/yarn/git/mvn/...)和元字符黑名单(&|;\$<>\n\r),但python/python3` 本身在白名单内;- 绕过方式(无需任何被禁字符):
python -c "__import__('os').system('id')"——-c字符串中不含被黑名单拦截的字符即可任意命令执行; ProcessBuilder(cmdList)以应用进程身份执行,可读取/etc/passwd、写 crontab / 反弹 shell / 读取应用配置中所有密钥。
- 利用条件: 无(匿名即可)
- 影响: 服务器完全失陷——RCE、提权、横向移动
- 修复建议:
- 立即将
/coding/**从security.excludes移除,必须登录; - 给
CodingController增加@SaCheckPermission鉴权,并按用户隔离工作区; ExecuteCommandTool移除python/python3白名单或改用严格 argv 模板(禁止-c/-m),对不可信命令一律拒绝。
- 立即将
C-2 未认证任意文件读写 + 代理 RCE —— /coding/chat
- 位置:
ruoyi-modules/ruoyi-chat/.../service/coding/impl/CodingServiceImpl.java:94-112,145-150 - 触发链:
POST /coding/chat携带workspacePath与prompt(匿名可访问);resolveWorkspace(bo.getWorkspacePath())对路径完全无限制:传workspacePath="/"即返回根目录Paths.get("/");- 以 root=
/构建 ReadFileTool / WriteFileTool / EditFileTool / DeleteFileTool / ExecuteCommandTool 五个工具并注入 Langchain4j agent(CodingServiceImpl.java:98-109); - 通过自然语言指示 agent 调用工具(如「读取 /etc/shadow」「写 /root/.ssh/authorized_keys」「执行 id」),即获得任意文件读写 + 命令执行能力。
- 对比: 同包的
CodingWorkspaceService.resolveRoot()(CodingWorkspaceService.java:74-82)做了工作区强校验,但CodingServiceImpl.resolveWorkspace()漏掉该校验,两处实现不一致。 - 利用条件: 无(匿名即可)
- 影响: 与 C-1 相同,服务器完全失陷
- 修复建议:
/coding/**移除出白名单并加权限校验(同 C-1);resolveWorkspace()复用CodingWorkspaceService.resolveRoot()的强校验逻辑,拒绝任意绝对路径;- 为 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.ymlsecurity.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默认口令。
- 第 62-64 行:MySQL
- 风险: 若数据库端口或监控台端口对外暴露,等于默认口令直达数据库。
- 修复建议: 改用环境变量注入并强制覆盖默认值;部署时校验禁止空口令/
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)未做路径白名单,存在环境差异与潜在注入面。
六、修复优先级建议(落地顺序)
- 本周内(阻断)
- 将
/coding/**、/workflow/run移出security.excludes,补权限注解与鉴权; - 修复
CodingServiceImpl.resolveWorkspace()工作区强校验(复用resolveRoot); ExecuteCommandTool去掉python/python3白名单或禁止-c/-m参数。
- 将
- 两周内(止血)
- OSS
fileUpload补权限 + 文件名 normalize 校验; - 模型
modelList补权限 +apiKey脱敏; - 会话读写做
userId + sessionId归属校验; - 工作流 HttpRequestNode 加 SSRF 防护;
- 清理硬编码 root/root、默认 admin、监控台默认口令。
- OSS
- 一个月内(加固)
- Redis Jackson 默认类型改为白名单验证器;
- MCP 工具、WebSocket、日志与上传类型全面加固;
- 密钥统一迁移到环境变量/密钥管理。
七、附录:审计方法与覆盖
- 方法: 5 路并行语义审查(SQL 注入 / 反序列化+RCE / SSRF+文件上传下载 / 硬编码凭据+认证绕过 / XSS+其它注入),每个高价值发现均回读源码逐行确认(含行号)。
- 未发现: 项目已从依赖树中排除 fastjson;日志框架为 Logback(非 Log4j2),未见对应已知 CVE 链。
- 声明: 本报告为授权防御性审计,所有结论基于静态代码分析,未进行实际漏洞验证(未发起任何攻击流量);利用条件均基于代码路径推导。
正文完