Files
erp/docs/audits/audit-2026-04-18-full.md
iven 841766b168
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
2026-04-19 08:46:28 +08:00

383 lines
15 KiB
Markdown
Raw 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.

# ERP 平台全面深度审计报告
> 审计日期: 2026-04-18
> 审计方式: 前端功能链路 + API 接口测试 + 代码静态分析 + 安全渗透测试
> 审计范围: 全部 5 个业务模块 + 2 个插件 + 前端 SPA + 安全与代码质量
---
## 一、审计总结
| 维度 | CRITICAL | HIGH | MEDIUM | LOW | 合计 |
|------|----------|------|--------|-----|------|
| 安全 | 2 | 4 | 3 | 2 | 11 |
| 功能 | 1 | 3 | 3 | 1 | 8 |
| 代码质量 | 0 | 1 | 3 | 6 | 10 |
| **合计** | **3** | **8** | **9** | **9** | **29** |
---
## 二、CRITICAL — 必须立即修复
### C-01 Redis 凭据硬编码在配置文件中(泄露到 Git
- **文件**: `crates/erp-server/config/default.toml` (line 11)
- **现象**: `url = "redis://:redis_KBCYJk@129.204.154.246:6379"` 硬编码了远程 Redis 密码和 IP
- **影响**: 凭据已提交到 Git 仓库,任何有代码访问权限的人都能获取 Redis 密码和服务器 IP
- **修复**:
1. 立即轮换 Redis 密码
2.`url` 改回 `__MUST_SET_VIA_ENV__` 占位符
3. 使用环境变量 `ERP__REDIS__URL` 传递
### C-02 存储型 XSS — 用户输入未做 HTML 清理
- **文件**: `crates/erp-auth/src/service/user_service.rs` (创建/更新用户)
- **现象**: 通过 `POST /api/v1/users` 可将 `<script>alert('xss')</script>``<img src=x onerror=alert(1)>` 直接存入数据库的 `display_name``email` 等字段
- **影响**:
- React JSX 自动转义避免了前端直接触发(当前安全)
- 但原始 HTML 已存储在数据库中,在以下场景可触发:
- 邮件模板渲染
- PDF 导出
- OpenAPI 文档中的 schema 示例
- 未来使用非 React 渲染的任何场景
- **验证**:
```
POST /api/v1/users {"display_name":"<img src=x onerror=alert(document.cookie)>"}
→ 201 Created, 原始 HTML 直接入库
```
- **修复**: 后端入库前对所有用户可编辑字段 strip HTML tags 或 escape HTML entities
### C-03 首页工作台统计卡片永久 Loading
- **文件**: `apps/web/src/pages/Home.tsx`
- **现象**: 4 个统计卡片(用户总数、角色数量、流程实例、未读消息)始终显示 loading 动画
- **根因**: `useCountUp` 动画依赖数据加载API 返回格式与前端预期不匹配
- **影响**: 工作台页面无法展示核心统计数据,用户体验极差
- **修复**: 修正统计 API 的数据格式,确保与 `StatisticCard` 组件预期一致
---
## 三、HIGH — 高优先级问题
### H-01 用户名唯一性约束未生效
- **文件**: `crates/erp-auth/src/service/user_service.rs` (创建用户)
- **现象**: 用相同 `username` 创建两次用户均返回 `201 Created`
- **影响**: 可能导致身份混淆、审计日志混乱
- **修复**: 在创建用户前先查询 `username` 是否已存在(同 tenant_id + 未删除),或添加数据库唯一索引
### H-02 消息模板 API 返回空 body
- **文件**: `GET /api/v1/messages/templates`
- **现象**: 返回空 HTTP body非 JSON 格式),前端无法解析
- **影响**: 消息中心"模板"tab 无法展示数据
- **修复**: 修复空列表的序列化处理,确保返回 `{"success":true,"data":{"data":[],"total":0}}`
### H-03 主题 API 返回空 body
- **文件**: `GET /api/v1/config/theme`
- **现象**: 返回空 body 而非 JSON
- **影响**: 主题设置页面无法加载当前配置
- **修复**: 为新租户初始化默认主题配置,或 API 返回默认值
### H-04 JWT Token 体积过大
- **文件**: `crates/erp-auth/src/service/token_service.rs`
- **现象**: Access Token 包含 64 个权限字符串JWT payload 约 2.5KB
- **影响**:
- 每次 HTTP 请求都要携带 2.5KB+ 的 Authorization header
- 影响带宽和性能,尤其在高频 API 调用场景
- 权限变更需要等 Token 过期才生效(最长 15 分钟)
- **修复**:
1. 方案 AJWT 只存角色,权限在服务端 Redis 缓存实时查询
2. 方案 B使用权限位图/bitmask 压缩
3. 方案 C减少 JWT 中的权限列表,改为中间件实时校验
### H-05 字段长度无限制
- **文件**: `crates/erp-auth/src/dto.rs`
- **现象**: `display_name` 可接受 500+ 字符(测试通过 500 个 'A'),无 max length 验证
- **影响**: UI 布局破坏、潜在数据库性能问题
- **修复**: 添加 `#[validate(length(max = 100))]` 等长度约束
---
## 四、MEDIUM — 中优先级问题
### M-01 菜单配置前端硬编码,未使用后端 API
- **文件**: `apps/web/src/components/AppLayout.tsx` 或路由配置
- **现象**: 后端 `GET /api/v1/config/menus` 返回空数组,侧边栏菜单完全前端硬编码
- **影响**: 菜单无法通过管理后台动态配置,插件菜单需要在代码中手动添加
- **修复**: 实现前端从 API 动态加载菜单配置,或在后端初始化默认菜单数据
### M-02 时间戳未本地化显示
- **文件**: 消息中心、审计日志等列表页面
- **现象**: 时间显示为原始 ISO 格式 `2026-04-14T13:10:59.516776Z`,用户不友好
- **影响**: 用户体验差
- **修复**: 使用 dayjs 格式化为本地时间,如 `2026-04-14 21:10:59`
### M-03 前端路由仅做认证守卫,无权限守卫
- **文件**: `apps/web/src/App.tsx`
- **现象**: 路由只检查是否已登录token 存在),不检查用户是否有权限访问特定页面
- **影响**: 无权限用户可以通过直接输入 URL 访问任何页面(虽然 API 层会返回 403
- **修复**: 在路由守卫中增加权限校验,根据 JWT 中的 permissions 控制页面可见性
### M-04 消息响应包含内部 tenant_id
- **文件**: `crates/erp-message/src/handler/message_handler.rs`
- **现象**: `GET /api/v1/messages` 返回每条消息的 `tenant_id` 字段
- **影响**: 泄露内部多租户架构信息
- **修复**: 在 DTO 层排除 `tenant_id` 字段
### M-05 搜索缺少防抖Debounce
- **文件**: `apps/web/src/pages/Users.tsx`, `apps/web/src/components/EntitySelect.tsx`
- **现象**: 用户搜索输入框每次按键都触发 API 请求
- **影响**: 高频请求冲击服务器,用户体验差
- **修复**: 添加 300ms debounce已有 `useDebouncedValue` Hook 但未使用)
### M-06 Organizations 页面 useCallback/useEffect 循环依赖
- **文件**: `apps/web/src/pages/Organizations.tsx`
- **现象**: useCallback 依赖项导致 useEffect 无限循环渲染
- **影响**: 性能问题、可能导致浏览器卡顿
- **修复**: 重构 useCallback 依赖项,消除循环
### M-07 测试数据残留在生产数据库
- **现象**: 数据库中存在以下测试用户和数据:
- `xss_user` — display_name 为 `<script>alert('xss')</script>`
- `test_role_api` — 测试角色
- `audit_test_user` — 审计测试用户
- `testuser01` — 测试用户
- `test_user_api` — API 测试用户
- `Perf test` 消息 — 性能测试消息
- `business_key: PERF-TEST-26311` 的待办任务
- **影响**: 数据污染、潜在安全风险
- **修复**: 清理所有测试数据,确保数据库只包含有意义的业务数据
### M-08 系统设置多个 tab 数据为空
- **现象**: 数据字典、编号规则、系统参数、语言管理等 tab 均无种子数据
- **影响**: 系统看起来像空的,用户需要手动配置所有基础数据
- **修复**: 在 `on_tenant_created` 中初始化默认字典(客户类型、行业、地区等)
### M-09 中文 API 响应编码异常
- **现象**: 部分中文内容在 API JSON 响应中显示为乱码(如 `\u7eef\u8364\u7cba` 而非"系统管理员"
- **影响**: 可能是 curl 的显示问题,也可能是后端序列化配置问题
- **修复**: 确认后端 JSON 序列化使用 `ensure_ascii: false` 等效配置
---
## 五、LOW — 低优先级 / 代码质量
### L-01 死代码 — graph 目录
- **文件**: `apps/web/src/pages/plugins/graph/` (6 个文件)
- **现象**: 完全未使用的代码
- **修复**: 删除或标记为实验性
### L-02 死代码 — 未使用的 Hooks
- **文件**: `apps/web/src/hooks/`
- **现象**: `useDarkMode`, `useDebouncedValue`, `usePaginatedData`, `useApiRequest` 4 个 Hook 未被引用
- **修复**: 清理或接入这些 Hook特别是 useDebouncedValue 在搜索场景很有用)
### L-03 重复代码 — useCountUp
- **现象**: `useCountUp` 在 3 处重复定义
- **修复**: 提取为共享 Hook
### L-04 暗色模式检测逻辑重复
- **现象**: `const isDark = token.colorBgContainer === '#111827'` 在 20+ 组件中重复
- **修复**: 用已有的 `useDarkMode` Hook 替换
### L-05 i18n 已配置但未使用
- **现象**: i18next 已初始化且有 30 个翻译 key但所有页面组件硬编码中文
- **修复**: 逐步替换硬编码中文为 i18n key
### L-06 antd 废弃 API 警告
- **现象**: `Drawer` 的 `width` 属性、`Modal` 的 `destroyOnClose` 已废弃
- **修复**: 升级到 antd 6 的新 API 用法
### L-07 ErrorBoundary 错误信息泄露
- **文件**: `apps/web/src/components/ErrorBoundary.tsx`
- **现象**: 错误边界展示完整的错误堆栈给用户
- **修复**: 生产环境只显示友好错误消息,堆栈信息仅记录到控制台
### L-08 Home 页面使用 dangerouslySetInnerHTML
- **文件**: `apps/web/src/pages/Home.tsx`
- **现象**: 工作台页面使用 `dangerouslySetInnerHTML` 渲染内容
- **影响**: 如果内容包含用户输入,可能导致 XSS
- **修复**: 改用 React 组件渲染
### L-09 插件恢复计数不准确
- **现象**: `Plugin recovered: 0` 但实际 WASM 已加载
- **修复**: 修正 recovery 计数逻辑
---
## 六、安全测试矩阵
| 测试项 | 结果 | 备注 |
|--------|------|------|
| 无 Token 访问受保护端点 | ✅ 401 | 正确拦截 |
| 无效 Token | ✅ 401 | 正确拦截 |
| 篡改 Token payload | ✅ 401 | HMAC 签名校验有效 |
| 错误密码登录 | ✅ 401 | 正确拒绝 |
| 短密码创建用户 | ✅ 400 | 验证 min=6 生效 |
| 空 Token 刷新 | ✅ 401 | 正确拒绝 |
| 旧 Refresh Token 重用 | ✅ 401 | 轮换机制生效 |
| SQL 注入(搜索参数) | ✅ 安全 | SeaORM 参数化查询 |
| SQL 注入UUID 路径) | ✅ 安全 | UUID 解析拒绝非法字符 |
| 存储型 XSS | ❌ 入库 | 后端未清理 HTMLReact 前端安全 |
| 无权限用户访问 API | ✅ 403 | 权限校验正确 |
| 无权限用户提权 | ✅ 403 | 角色分配受权限保护 |
| 限流 | ✅ 生效 | 5 次失败后触发 429 |
| CORS 配置 | ✅ 白名单 | 仅允许 localhost 端口 |
| 凭据泄露 | ❌ Redis 密码硬编码 | 已提交到 Git |
---
## 七、功能链路审计结果
### 7.1 认证链路 ✅ 基本正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 登录 → JWT 签发 | ✅ | access (15min) + refresh (7d) |
| Token 刷新轮换 | ✅ | 旧 Token 使用后立即失效 |
| 密码修改 → Token 吊销 | ✅ | 所有 refresh token 失效 |
| 登出 → Token 吊销 | ✅ | |
| 限流保护 | ✅ | 5 次失败后 429 |
| 审计日志记录 | ✅ | 登录成功/失败均有记录 |
### 7.2 用户管理 ✅ 基本正常,有缺陷
| 环节 | 状态 | 备注 |
|------|------|------|
| CRUD 操作 | ✅ | |
| 角色分配 | ✅ | |
| 用户名唯一性 | ❌ | 重复用户名可创建 |
| 输入验证 | ⚠️ | 密码有验证,其他字段长度/XSS 无验证 |
| 软删除 | ✅ | |
### 7.3 权限管理 ✅ 正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 角色列表 | ✅ | 3 个角色admin/viewer/test |
| 权限分配 | ✅ | 54 个权限可精确分配 |
| 系统角色保护 | ⚠️ | admin 角色权限可被修改 |
| data_scope 配置 | ❌ | 权限对话框中无 data_scope 配置入口 |
### 7.4 工作流引擎 ✅ 正常
| 环节 | 状态 | 备注 |
|------|------|------|
| 流程定义 CRUD | ✅ | 3 个定义draft/published |
| 流程发起 | ✅ | |
| 任务审批 | ✅ | approve/reject |
| 任务委派 | ✅ | |
| 实例监控 | ✅ | running/terminated/suspended |
| 超时检测 | ✅ | 60s 间隔扫描 |
### 7.5 消息中心 ⚠️ 部分异常
| 环节 | 状态 | 备注 |
|------|------|------|
| 消息列表 | ✅ | 10 条消息 |
| 未读计数 | ✅ | bell 图标显示未读数 |
| 标记已读 | ✅ | |
| 全部已读 | ✅ | |
| 消息模板 | ❌ | API 返回空 body |
| 通知设置 | ⚠️ | 未验证 |
| 工作流事件 → 消息 | ✅ | "流程已启动" 消息自动生成 |
### 7.6 系统配置 ⚠️ 数据缺失
| 环节 | 状态 | 备注 |
|------|------|------|
| 数据字典 | ❌ | 空数据,无种子 |
| 菜单配置 | ❌ | 后端空,前端硬编码 |
| 编号规则 | ❌ | 空 |
| 系统参数 | ⚠️ | 未验证 |
| 主题设置 | ❌ | API 返回空 |
| 语言管理 | ⚠️ | 未验证 |
| 修改密码 | ✅ | 功能正常 |
| 审计日志 | ✅ | |
### 7.7 插件系统 ✅ 基本正常
| 环节 | 状态 | 备注 |
|------|------|------|
| CRM 插件运行 | ✅ | 状态:运行中 |
| 客户 CRUD | ✅ | 6 条客户数据 |
| 联系人 | ✅ | |
| 沟通记录 | ✅ | |
| 标签管理 | ✅ | |
| 客户关系 | ✅ | |
| 统计概览 | ⚠️ | |
| 销售漏斗 | ⚠️ | |
---
## 八、修复优先级建议
### 🔴 立即修复(本周内)
| 编号 | 问题 | 预计工作量 |
|------|------|-----------|
| C-01 | Redis 凭据从 config 移除,改用环境变量 | 0.5h |
| C-02 | 后端添加 HTML sanitize 中间件 | 2h |
| C-03 | 修复首页统计卡片数据格式 | 1h |
| H-01 | 添加用户名唯一性校验 | 1h |
### 🟡 本迭代修复2 周内)
| 编号 | 问题 | 预计工作量 |
|------|------|-----------|
| H-02 | 修复消息模板空返回 | 0.5h |
| H-03 | 修复主题 API 空返回 | 0.5h |
| H-04 | JWT 权限压缩或改为服务端查询 | 4h |
| H-05 | 添加字段长度验证 | 1h |
| M-03 | 添加前端路由权限守卫 | 2h |
| M-05 | 搜索添加防抖 | 0.5h |
| M-07 | 清理测试数据 | 1h |
| M-08 | 初始化默认系统配置数据 | 2h |
### 🟢 迭代中逐步修复
| 编号 | 问题 |
|------|------|
| M-01 | 菜单动态加载 |
| M-02 | 时间戳本地化 |
| M-04 | API 响应排除 tenant_id |
| M-06 | Organizations 性能修复 |
| L-01~L-09 | 代码质量清理 |
---
## 九、系统亮点(做得好的地方)
1. **Token 刷新轮换机制** — 旧 Refresh Token 重用被正确拒绝
2. **限流保护** — 登录失败 5 次后触发 429 Too Many Requests
3. **SeaORM 参数化查询** — SQL 注入测试全部被拦截
4. **权限校验完整性** — 无权限用户所有操作返回 403
5. **多租户架构** — JWT 注入 tenant_id中间件自动过滤
6. **审计日志** — 登录/登出/密码修改等关键操作有完整记录
7. **WASM 插件沙箱** — CRM 插件运行稳定6 个实体全部可用
8. **工作流引擎** — BPMN 解析、Token 驱动、任务分配完整实现
9. **错误处理链** — thiserror → AppError → HTTP 响应的统一错误体系
10. **优雅关闭** — CTRL+C 信号处理、模块按拓扑逆序关闭