fundplatform/doc/security-fixes.md
zhangjf 455a20c1df 完善项目配置和测试用例
新增内容:
1. 添加 AGENTS.md 和 CLAUDE.md AI 助手配置文件
2. 添加安全修复说明文档 (doc/security-fixes.md)
3. 新增单元测试用例:
   - fund-common: TenantContextHolderTest, UserContextHolderTest, PageResultTest, ResultTest
   - fund-sys: AuthServiceImplTest, RoleServiceImplTest, TenantServiceImplTest

修改内容:
1. 数据库初始化脚本更新 (fund_sys_init.sql)
2. 前端依赖更新 (package.json)
3. 登录和密码管理功能优化:
   - 管理后台和移动端登录页面
   - 密码修改功能
4. 租户上下文处理优化 (TenantLineHandlerImpl)
5. 网关过滤器增强:
   - TenantGatewayFilter 租户过滤
   - TokenAuthFilter 认证过滤
6. Controller 层代码优化
7. DTO 和 Service 层代码改进

技术改进:
- 密码加密方式从 BCrypt 改为 MD5(前后端一致)
- 登录验证流程优化,支持多租户
- 增加日志输出便于调试
- 代码规范性和可维护性提升
2026-03-01 19:06:42 +08:00

169 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 租户隔离安全漏洞修复记录
**修复日期**: 2026-03-01
**修复分支**: master
**修复范围**: 多租户数据隔离机制
---
## 漏洞一:网关未覆盖客户端伪造的 X-Tenant-Id高危
### 漏洞描述
`TokenAuthFilter` 在 Token 验证通过后,使用 Spring WebFlux 的 `request.mutate().header()` 将 Token 中的 tenantId 写入请求头。然而 WebFlux 的 `header()` 方法是**追加**而非替换语义,若客户端预先在请求中设置了 `X-Tenant-Id`,下游服务从 `getFirst()` 取到的仍然是客户端伪造的值。攻击者可以通过在请求中携带任意 `X-Tenant-Id` 来访问其他租户的数据。
### 攻击场景
```
攻击者发送请求:
Authorization: Bearer <合法Token属于租户A>
X-Tenant-Id: 999 ← 伪造的租户ID
```
原始代码中,下游 `ContextInterceptor` 从请求头取到的 `X-Tenant-Id` 可能是 999导致 `TenantContextHolder` 被设置为 999进而 MyBatis-Plus 租户插件以 tenant_id=999 过滤 SQL越权访问其他租户数据。
### 修复方案
`TokenAuthFilter` 中先移除客户端传来的 `X-Tenant-Id`,再写入 Token 中已认证的值:
```java
// 安全修复:先移除客户端传来的 X-Tenant-Id再写入 Token 中已认证的值
ServerHttpRequest mutatedRequest = request.mutate()
.headers(headers -> headers.remove(TENANT_ID_HEADER)) // 先删除客户端值
.header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId()))
.header(USERNAME_HEADER, tokenInfo.getUsername())
.header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId())) // 再写入认证值
.build();
```
### 修复位置
`fund-gateway/src/main/java/com/fundplatform/gateway/filter/TokenAuthFilter.java` 第 86-93 行
---
## 漏洞二IGNORE_TABLES 包含业务敏感表(高危)
### 漏洞描述
`TenantLineHandlerImpl.IGNORE_TABLES` 中包含了 `sys_user``sys_role``sys_dept``sys_config` 等业务表,这些表均有 `tenant_id` 字段属于租户私有数据。将其加入忽略列表后MyBatis-Plus 不会为这些表的查询自动注入 `tenant_id` 过滤条件,导致以下问题:
1. **跨租户用户数据泄漏**:查询 `sys_user` 时返回所有租户的用户数据
2. **跨租户角色泄漏**:查询 `sys_role` 时返回所有租户的角色配置
3. **跨租户部门泄漏**:查询 `sys_dept` 时返回所有租户的部门结构
### 修复方案
`IGNORE_TABLES` 中仅保留真正全平台共享的静态数据表:
```java
// 修复后:仅保留平台级全局共享表
private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList(
"sys_menu", // 菜单表(系统菜单结构全局共享)
"sys_dict", // 字典表(枚举数据全局共享)
"sys_log", // 日志表(独立存储逻辑)
"gen_table", // 代码生成表
"gen_table_column"
));
// 已移除sys_user、sys_role、sys_dept、sys_config均为租户私有数据
```
### 修复位置
`fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 34-46 行
---
## 漏洞三:租户 ID 缺失时 Fallback 到默认值 1中危
### 漏洞描述
`TenantLineHandlerImpl.getTenantId()` 中,当从 `TenantContextHolder` 获取不到租户 ID 时,代码回退到默认值 `1L`
```java
// 漏洞代码
if (tenantId == null) {
tenantId = 1L; // 危险误操作租户1的数据
}
```
这导致在没有认证上下文的场景如定时任务、内部错误等SQL 会意外查询租户1的数据可能造成租户1数据泄漏或误写入。
### 修复方案
当租户上下文缺失时直接抛出异常,中断 SQL 执行:
```java
// 修复后:缺失租户上下文时强制报错
if (tenantId == null) {
throw new IllegalStateException("[Security] 当前请求缺少租户上下文拒绝执行SQL");
}
```
### 修复位置
`fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 56-66 行
---
## 漏洞四:登录接口跨租户用户名混乱(中危)
### 漏洞描述
登录接口 `/auth/login` 在 Token 白名单中,`TenantContextHolder` 为空。由于 `sys_user` 原来在 `IGNORE_TABLES` 中(全局查询),登录时仅按用户名查询不区分租户:
```java
// 漏洞代码:不同租户可能有同名用户,查到哪个是不确定的
wrapper.eq(SysUser::getUsername, request.getUsername());
// 未加 tenantId 条件
SysUser user = userDataService.getOne(wrapper);
```
当不同租户有同名用户时,认证结果不确定,存在以下风险:
- 用户 A租户1可能以租户2用户的身份登录
- 登录成功后 Token 中绑定了错误的 tenantId
### 修复方案
1. `LoginRequestDTO` 增加 `tenantId` 字段(必填),前端登录时显式指定租户
2. 登录查询使用 `TenantIgnoreHelper` 跳过自动租户过滤,同时显式加入 `tenant_id` 条件:
```java
// 修复后:使用 TenantIgnoreHelper + 显式 tenantId 条件
SysUser user = TenantIgnoreHelper.ignore(() -> {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, request.getUsername());
wrapper.eq(SysUser::getTenantId, request.getTenantId()); // 显式限定租户
wrapper.eq(SysUser::getDeleted, 0);
return userDataService.getOne(wrapper);
});
```
### 修复位置
- `fund-sys/src/main/java/com/fundplatform/sys/dto/LoginRequestDTO.java` — 新增 tenantId 字段
- `fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java` 第 31-44 行
---
## 修复总结
| 编号 | 漏洞类型 | 危险级别 | 修复文件 |
|------|---------|---------|---------|
| 1 | 网关未覆盖客户端伪造的 X-Tenant-Id | 高危 | `fund-gateway/.../TokenAuthFilter.java` |
| 2 | IGNORE_TABLES 包含业务敏感表 | 高危 | `fund-common/.../TenantLineHandlerImpl.java` |
| 3 | 租户 ID 缺失时 fallback 到默认值 | 中危 | `fund-common/.../TenantLineHandlerImpl.java` |
| 4 | 登录查询未限定租户范围 | 中危 | `fund-sys/.../AuthServiceImpl.java`, `LoginRequestDTO.java` |
## 修复后的安全保证
1. **网关层**:经过 Token 验证的请求,`X-Tenant-Id` 由 Token 中的认证值强制覆盖,客户端无法伪造
2. **数据层**业务表sys_user、sys_role、sys_dept、sys_config均受 MyBatis-Plus 租户插件保护,自动注入 `tenant_id` 过滤
3. **兜底保护**:租户上下文为空时 SQL 拒绝执行,不会 fallback 到任何租户
4. **登录安全**:登录时必须指定 tenantId确保同名用户不会跨租户混乱认证
## 注意事项
- 所有需要跨租户操作的合法场景(如超管管理所有租户),必须显式使用 `TenantIgnoreHelper.ignore()` 包装,并在代码中注明安全意图
- 定时任务、事件监听器等异步场景在执行 DB 操作前,必须先设置 `TenantContextHolder`,执行完后在 finally 中清理