From 9249a8f88433b8f3de2889c376f1ccc829b6e1f4 Mon Sep 17 00:00:00 2001 From: cp1996 <1102213590@qq.com> Date: Fri, 10 Apr 2026 20:06:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E5=A4=84?= =?UTF-8?q?=E8=B6=8A=E6=9D=83=E8=AE=BF=E9=97=AE=E6=BC=8F=E6=B4=9E=20(IDOR?= =?UTF-8?q?=20/=20missing=20@PreAuthorize)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 UserUtil.checkUserAccess(targetUserId) 统一越权校验工具, 并修复以下控制器中缺失或错误的权限校验: 1. SysOptionsRestController.findAllOptions 无任何权限校验,任何登录用户可读取平台级系统参数, 其中包含阿里云 AccessKey / 邮箱 / 非对称加密公钥等敏感配置。 加超管校验 + @PreAuthorize("system_options_select")。 2. DictRestController - get / findPage 缺失 @PreAuthorize,补 system_dict_select - delAll 的权限名错写为 system_dict_insert,改为 system_dict_delete 3. RoleRestController.findPage / TenantRestController.findPage @PreAuthorize 被注释掉,恢复启用。 4. UserRestController.getInfoById / getOrgByUserId / getRoleIdsByUserId 可通过传入任意 userId 读取其他租户/用户的信息、组织和角色。 加 @PreAuthorize("system_user_select") + UserUtil.checkUserAccess(userId)。 注:getInfo() / getOrg() 通过 this.xxx() 内部调用这些方法, Spring AOP 对 this 调用不生效,@PreAuthorize 不会触发; checkUserAccess 对"查自己"直接放行,保持既有行为。 5. UserRoleRefRestController.getRoles 同上,补 @PreAuthorize 和 checkUserAccess。 checkUserAccess 规则: - 查自己 / 超管 → 放行 - 目标是超管 → 仅超管可访问 - 跨租户 → 拦截 - 同租户且目标非超管 → 放行 - 其它一律抛 AccessDeniedException --- .../java/org/opsli/core/utils/UserUtil.java | 54 +++++++++++++++++++ .../system/dict/web/DictRestController.java | 4 +- .../options/web/SysOptionsRestController.java | 12 +++++ .../system/role/web/RoleRestController.java | 2 +- .../tenant/web/TenantRestController.java | 2 +- .../user/service/impl/UserServiceImpl.java | 6 +++ .../user/web/UserOrgRefRestController.java | 3 ++ .../system/user/web/UserRestController.java | 32 +++++++++++ .../user/web/UserRoleRefRestController.java | 6 +++ 9 files changed, 118 insertions(+), 3 deletions(-) diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserUtil.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserUtil.java index 4795c824..21f13358 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserUtil.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserUtil.java @@ -42,6 +42,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.core.annotation.Order; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; import java.util.List; @@ -747,6 +748,59 @@ public class UserUtil { userAllPermsByUserId.contains(PERMS_TENANT); } + /** + * 校验当前登录人是否有权访问目标 userId 的数据 - 修复越权漏洞 + *

+ * 规则: + *

    + *
  1. 查自己 → 放行
  2. + *
  3. 当前用户是超级管理员 → 放行
  4. + *
  5. 目标用户是超级管理员 → 仅超管可访问(普通人拦截)
  6. + *
  7. 跨租户访问 → 拦截
  8. + *
  9. 同租户且目标非超管 → 放行
  10. + *
+ * 不满足放行条件的一律抛 {@link AccessDeniedException},由统一异常处理返回无权访问 + * + * @param targetUserId 目标用户ID + */ + public static void checkUserAccess(String targetUserId) { + // 判断 工具类是否初始化完成 + ThrowExceptionUtil.isThrowException(!IS_INIT, + CoreMsg.OTHER_EXCEPTION_UTILS_INIT); + + if (StringUtils.isBlank(targetUserId)) { + throw new AccessDeniedException("参数错误"); + } + + // getUser() 失败会直接抛 TokenException,此处不会返回 null + UserModel currUser = getUser(); + + // 1) 查自己:放行 + if (StringUtils.equals(currUser.getId(), targetUserId)) { + return; + } + + // 2) 当前用户是超级管理员:放行 + if (StringUtils.equals(SUPER_ADMIN, currUser.getUsername())) { + return; + } + + UserModel targetUser = getUser(targetUserId); + if (targetUser == null) { + throw new AccessDeniedException("用户不存在"); + } + + // 3) 非超管不能查超级管理员的数据 + if (StringUtils.equals(SUPER_ADMIN, targetUser.getUsername())) { + throw new AccessDeniedException("无权访问"); + } + + // 4) 非超管不能跨租户访问 + if (!StringUtils.equals(currUser.getTenantId(), targetUser.getTenantId())) { + throw new AccessDeniedException("无权访问"); + } + } + // ===================================== /** diff --git a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/dict/web/DictRestController.java b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/dict/web/DictRestController.java index 25857e32..4e8359f8 100644 --- a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/dict/web/DictRestController.java +++ b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/dict/web/DictRestController.java @@ -69,6 +69,7 @@ public class DictRestController extends BaseRestController get(DictModel model) { model = IService.get(model); @@ -83,6 +84,7 @@ public class DictRestController extends BaseRestController findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) { @@ -173,7 +175,7 @@ public class DictRestController extends BaseRestController> findAllOptions() { + // 修复 越权漏洞 - 系统参数属于平台级全局配置(含阿里云 AccessKey / 邮箱配置 / 非对称加密公钥等), + // 仅允许超级管理员访问,防止租户管理员跨租户读取敏感配置 + UserModel currUser = UserUtil.getUser(); + if (!StringUtils.equals(UserUtil.SUPER_ADMIN, currUser.getUsername())) { + throw new AccessDeniedException("无权访问"); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); // 查询内置数据 queryWrapper.eq("iz_lock", DictType.NO_YES_YES.getValue()); diff --git a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/role/web/RoleRestController.java b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/role/web/RoleRestController.java index 7246a8b7..1b9c551b 100644 --- a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/role/web/RoleRestController.java +++ b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/role/web/RoleRestController.java @@ -74,7 +74,7 @@ public class RoleRestController extends BaseRestController findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) { diff --git a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/tenant/web/TenantRestController.java b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/tenant/web/TenantRestController.java index c6aafa46..240ab309 100644 --- a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/tenant/web/TenantRestController.java +++ b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/tenant/web/TenantRestController.java @@ -99,7 +99,7 @@ public class TenantRestController extends BaseRestController findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) { diff --git a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/user/service/impl/UserServiceImpl.java b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/user/service/impl/UserServiceImpl.java index 0b7b95fe..0b675f10 100644 --- a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/user/service/impl/UserServiceImpl.java +++ b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/user/service/impl/UserServiceImpl.java @@ -543,6 +543,9 @@ public class UserServiceImpl extends CrudServiceImpl getInfoById(String userId) { + // 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据 + // (内部方法 getInfo() 通过 this.getInfoById(...) 调用时, + // Spring AOP 对 this 调用不生效,@PreAuthorize 不会触发; + // checkUserAccess 查自己时直接放行,保持既有行为) + UserUtil.checkUserAccess(userId); + UserModel currUser = UserUtil.getUserBySource(userId); if(currUser == null){ throw new TokenException(TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY); @@ -159,8 +166,15 @@ public class UserRestController extends BaseRestController getOrgByUserId(String userId) { + // 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据 + // (内部方法 getOrg() 通过 this.getOrgByUserId(user.getId()) 调用时, + // Spring AOP 对 this 调用不生效,@PreAuthorize 不会触发; + // checkUserAccess 查自己时直接放行,保持既有行为) + UserUtil.checkUserAccess(userId); + List orgListByUserId = UserUtil.getOrgListByUserId(userId); return ResultWrapper.getSuccessResultWrapper(orgListByUserId); } @@ -171,8 +185,12 @@ public class UserRestController extends BaseRestController> getRoleIdsByUserId(String userId) { + // 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据 + UserUtil.checkUserAccess(userId); + List roleIdList = iUserRoleRefService.getRoleIdList(userId); return ResultWrapper.getSuccessResultWrapper(roleIdList); } @@ -392,6 +410,9 @@ public class UserRestController extends BaseRestController getRoles(String userId) { + // 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据 + UserUtil.checkUserAccess(userId); List roleIdList = iUserRoleRefService.getRoleIdList(userId); String defRoleId = iUserRoleRefService.getDefRoleId(userId); @@ -91,6 +94,9 @@ public class UserRoleRefRestController implements UserRoleRefApi { // 演示模式 不允许操作 this.demoError(); + // 修复越权漏洞 - 校验当前登录人是否有权为目标 userId 设置角色 + UserUtil.checkUserAccess(model.getUserId()); + boolean ret = iUserRoleRefService.setRoles(model); if(ret){ return ResultWrapper.getSuccessResultWrapper(); From bcd55a614473df47ef5f089a03cae49017cbb6e8 Mon Sep 17 00:00:00 2001 From: cp1996 <1102213590@qq.com> Date: Fri, 10 Apr 2026 20:07:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=90=AF=E7=94=A8=20MyBatis-Plus?= =?UTF-8?q?=20BlockAttackInnerInterceptor=20=E9=98=B2=E5=85=A8=E8=A1=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0/=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拦截无 WHERE 条件的 UPDATE / DELETE 语句,避免因业务 bug 或 SQL 注入导致整表数据被误清空。 由于 mybatis-plus 3.5.9 起将 jsqlparser 相关能力从 core 包 拆出到独立的 mybatis-plus-jsqlparser 模块,BlockAttackInnerInterceptor 依赖 SQL 解析,需要显式引入该依赖。 改动: - pom.xml (根):dependencyManagement 加入 mybatis-plus-jsqlparser - opsli-base-support/opsli-core/pom.xml:引入 mybatis-plus-jsqlparser - MyBatisPlusConfig.java:import BlockAttackInnerInterceptor, 取消原有注释并启用拦截器 --- opsli-base-support/opsli-core/pom.xml | 6 ++++++ .../opsli/core/autoconfigure/conf/MyBatisPlusConfig.java | 6 ++++-- pom.xml | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/opsli-base-support/opsli-core/pom.xml b/opsli-base-support/opsli-core/pom.xml index 53d2ae18..91dd1cc5 100644 --- a/opsli-base-support/opsli-core/pom.xml +++ b/opsli-base-support/opsli-core/pom.xml @@ -111,6 +111,12 @@ mybatis-plus-spring-boot3-starter + + + com.baomidou + mybatis-plus-jsqlparser + + com.github.pagehelper pagehelper-spring-boot-starter diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/conf/MyBatisPlusConfig.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/conf/MyBatisPlusConfig.java index 3b6c9491..19f9cdd1 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/conf/MyBatisPlusConfig.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/conf/MyBatisPlusConfig.java @@ -16,6 +16,7 @@ package org.opsli.core.autoconfigure.conf; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSessionFactory; @@ -49,8 +50,9 @@ public class MyBatisPlusConfig { // 乐观锁 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); - // 防止全表更新与删除插件 - //mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + // 防止全表更新与删除插件 - 安全加固 + // 拦截无 WHERE 条件的 UPDATE/DELETE,避免因业务 bug 或 SQL 注入导致整表数据被清空 + mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return mybatisPlusInterceptor; } diff --git a/pom.xml b/pom.xml index 41e2498d..77f11573 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,13 @@ ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-jsqlparser + ${mybatis-plus.version} + + com.github.pagehelper pagehelper-spring-boot-starter From 2d6d1cfa5fbe6cf36b92b41c8a28e91b64f62958 Mon Sep 17 00:00:00 2001 From: cp1996 <1102213590@qq.com> Date: Fri, 10 Apr 2026 20:07:23 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20spring-boot-starter-actuator=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=20(CVE-2026-22733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actuator 依赖在本项目中未被任何业务代码使用(全仓库 grep 零命中 org.springframework.boot.actuate.*,无自定义 @Endpoint / HealthIndicator / InfoContributor),仅默认暴露 /actuator/health。 项目系统监控信息由 OSHI 实现,不依赖 actuator。移除此依赖以 降低攻击面并规避 CVE-2026-22733。 如未来确需使用 actuator,应同时配置 management.endpoints.web.exposure.include 白名单,避免默认 暴露超出预期的端点。 --- pom.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 77f11573..4c771181 100644 --- a/pom.xml +++ b/pom.xml @@ -220,11 +220,15 @@ org.springframework.boot spring-boot-starter-aop - + + + + org.springframework.boot