Merge pull request #46 from cp1996-bf/fix/security-hardening

已合并修复方案到 development,关闭此 PR。感谢贡献!
development
Pace Zhou 3 weeks ago committed by GitHub
commit 64ef1b2325
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -111,6 +111,12 @@
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- mybatis-plus SQL 解析模块3.5.9 起拆出BlockAttackInnerInterceptor 必需 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>

@ -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;
}

@ -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 -
* <p>
*
* <ol>
* <li> </li>
* <li> </li>
* <li> 访</li>
* <li>访 </li>
* <li> </li>
* </ol>
* {@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("无权访问");
}
}
// =====================================
/**

@ -69,6 +69,7 @@ public class DictRestController extends BaseRestController<SysDict, DictModel, I
* @return ResultWrapper
*/
@Operation(summary = "获得单条字典数据 - ID")
@PreAuthorize("hasAuthority('system_dict_select')")
@Override
public ResultWrapper<DictModel> get(DictModel model) {
model = IService.get(model);
@ -83,6 +84,7 @@ public class DictRestController extends BaseRestController<SysDict, DictModel, I
* @return ResultWrapper
*/
@Operation(summary = "获得分页数据 - 查询构造器")
@PreAuthorize("hasAuthority('system_dict_select')")
@Override
public ResultWrapper<?> findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) {
@ -173,7 +175,7 @@ public class DictRestController extends BaseRestController<SysDict, DictModel, I
* @return ResultWrapper
*/
@Operation(summary = "批量删除字典数据")
@PreAuthorize("hasAuthority('system_dict_insert')")
@PreAuthorize("hasAuthority('system_dict_delete')")
@OperateLogger(description = "批量删除字典数据",
module = ModuleEnum.MODULE_DICT, operationType = OperationTypeEnum.DELETE, db = true)
@Override

@ -28,9 +28,11 @@ import opsli.plugins.crypto.CryptoPlugin;
import opsli.plugins.crypto.enums.CryptoAsymmetricType;
import opsli.plugins.crypto.model.CryptoAsymmetric;
import opsli.plugins.crypto.strategy.CryptoAsymmetricService;
import org.apache.commons.lang3.StringUtils;
import org.opsli.api.base.result.ResultWrapper;
import org.opsli.api.web.system.options.OptionsApi;
import org.opsli.api.wrapper.system.options.OptionsModel;
import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.annotation.ApiRestController;
import org.opsli.common.enums.DictType;
import org.opsli.common.utils.WrapperUtil;
@ -42,8 +44,10 @@ import org.opsli.core.persistence.Page;
import org.opsli.core.persistence.querybuilder.QueryBuilder;
import org.opsli.core.persistence.querybuilder.WebQueryBuilder;
import org.opsli.core.utils.OptionsUtil;
import org.opsli.core.utils.UserUtil;
import org.opsli.modulars.system.options.entity.SysOptions;
import org.opsli.modulars.system.options.service.ISysOptionsService;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.multipart.MultipartHttpServletRequest;
@ -292,7 +296,15 @@ public class SysOptionsRestController extends BaseRestController<SysOptions, Opt
* @return ResultWrapper
*/
@Override
@PreAuthorize("hasAuthority('system_options_select')")
public ResultWrapper<Map<String, OptionsModel>> findAllOptions() {
// 修复 越权漏洞 - 系统参数属于平台级全局配置(含阿里云 AccessKey / 邮箱配置 / 非对称加密公钥等),
// 仅允许超级管理员访问,防止租户管理员跨租户读取敏感配置
UserModel currUser = UserUtil.getUser();
if (!StringUtils.equals(UserUtil.SUPER_ADMIN, currUser.getUsername())) {
throw new AccessDeniedException("无权访问");
}
QueryWrapper<SysOptions> queryWrapper = new QueryWrapper<>();
// 查询内置数据
queryWrapper.eq("iz_lock", DictType.NO_YES_YES.getValue());

@ -74,7 +74,7 @@ public class RoleRestController extends BaseRestController<SysRole, RoleModel, I
* @return ResultWrapper
*/
@Operation(summary = "获得分页数据 - 查询构造器")
//@PreAuthorize("hasAuthority('system_role_select')")
@PreAuthorize("hasAuthority('system_role_select')")
@Override
public ResultWrapper<?> findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) {

@ -99,7 +99,7 @@ public class TenantRestController extends BaseRestController<SysTenant, TenantMo
* @return ResultWrapper
*/
@Operation(summary = "获得分页数据 - 查询构造器")
//@PreAuthorize("hasAuthority('system_tenant_select')")
@PreAuthorize("hasAuthority('system_tenant_select')")
@Override
public ResultWrapper<?> findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) {

@ -543,6 +543,9 @@ public class UserServiceImpl extends CrudServiceImpl<UserMapper, SysUser, UserMo
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updatePasswordByNotCheckOld(ToUserPassword toUserPassword) {
// 修复越权漏洞 - 校验当前登录人是否有权修改目标 userId 的密码
UserUtil.checkUserAccess(toUserPassword.getUserId());
UserModel userModel = super.get(toUserPassword.getUserId());
// 如果为空则 不修改密码
if(userModel == null){
@ -659,6 +662,9 @@ public class UserServiceImpl extends CrudServiceImpl<UserMapper, SysUser, UserMo
@Override
@Transactional(rollbackFor = Exception.class)
public boolean resetPassword(UserPassword userPassword) {
// 修复越权漏洞 - 校验当前登录人是否有权重置目标 userId 的密码
UserUtil.checkUserAccess(userPassword.getUserId());
UserModel userModel = super.get(userPassword.getUserId());
// 如果为空则 不修改密码
if(userModel == null){

@ -72,6 +72,9 @@ public class UserOrgRefRestController implements UserOrgRefApi {
// 演示模式 不允许操作
this.demoError();
// 修复越权漏洞 - 校验当前登录人是否有权为目标 userId 设置组织
UserUtil.checkUserAccess(model.getUserId());
boolean ret = iUserOrgRefService.setOrg(model);
if(!ret){
// 权限设置失败

@ -97,8 +97,15 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
* @return ResultWrapper
*/
@Operation(summary = "当前登陆用户信息 By Id")
@PreAuthorize("hasAuthority('system_user_select')")
@Override
public ResultWrapper<UserInfo> 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<SysUser, UserModel, I
* @return ResultWrapper
*/
@Operation(summary = "用户组织机构")
@PreAuthorize("hasAuthority('system_user_select')")
@Override
public ResultWrapper<?> getOrgByUserId(String userId) {
// 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据
// (内部方法 getOrg() 通过 this.getOrgByUserId(user.getId()) 调用时,
// Spring AOP 对 this 调用不生效,@PreAuthorize 不会触发;
// checkUserAccess 查自己时直接放行,保持既有行为)
UserUtil.checkUserAccess(userId);
List<UserOrgRefModel> orgListByUserId = UserUtil.getOrgListByUserId(userId);
return ResultWrapper.getSuccessResultWrapper(orgListByUserId);
}
@ -171,8 +185,12 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
* @return ResultWrapper
*/
@Operation(summary = "根据 userId 获得用户角色Id集合")
@PreAuthorize("hasAuthority('system_user_select')")
@Override
public ResultWrapper<List<String>> getRoleIdsByUserId(String userId) {
// 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据
UserUtil.checkUserAccess(userId);
List<String> roleIdList = iUserRoleRefService.getRoleIdList(userId);
return ResultWrapper.getSuccessResultWrapper(roleIdList);
}
@ -392,6 +410,9 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
// 验证对象
ValidatorUtil.verify(enableUserModel);
// 修复越权漏洞 - 校验当前登录人是否有权操作目标 userId
UserUtil.checkUserAccess(enableUserModel.getUserId());
// 变更账户状态
boolean lockAccountFlag = IService.enableAccount(
enableUserModel.getUserId(), enableUserModel.getEnabled());
@ -562,6 +583,9 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
// 转换模型
String id = JSONUtil.parseObj(asymmetricDecryptToObj).getStr("id");
// 修复越权漏洞 - 校验当前登录人是否有权删除目标用户
UserUtil.checkUserAccess(id);
IService.delete(id);
return ResultWrapper.getSuccessResultWrapperByMsg("删除用户信息成功");
@ -587,6 +611,14 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
String ids = JSONUtil.parseObj(asymmetricDecryptToObj).getStr("ids");
String[] idArray = Convert.toStrArray(ids);
// 修复越权漏洞 - 逐个校验当前登录人是否有权删除目标用户
if(idArray != null){
for(String uid : idArray){
UserUtil.checkUserAccess(uid);
}
}
IService.deleteAll(idArray);
return ResultWrapper.getSuccessResultWrapperByMsg("批量删除用户信息成功");

@ -64,8 +64,11 @@ public class UserRoleRefRestController implements UserRoleRefApi {
* @return ResultWrapper
*/
@Operation(summary = "根据 userId 获得用户角色Id集合")
@PreAuthorize("hasAuthority('system_user_select')")
@Override
public ResultWrapper<UserRoleRefModel> getRoles(String userId) {
// 修复 越权漏洞 - 校验当前登录人是否有权访问目标 userId 的数据
UserUtil.checkUserAccess(userId);
List<String> 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();

@ -148,6 +148,13 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- mybatis-plus SQL 解析模块3.5.9 起拆出BlockAttackInnerInterceptor 必需 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
@ -213,11 +220,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- actuator 端点监控 -->
<!-- 修复 CVE-2026-22733 移除 actuator项目系统监控使用 OSHI 实现,不依赖此组件) -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>

Loading…
Cancel
Save