Files
user-system/docs/PROJECT_REVIEW_REPORT.md

466 lines
16 KiB
Markdown
Raw Permalink Normal View History

# 项目严格 Review 报告
更新日期2026-03-29
审查范围:`D:\project` 当前工作代码、配置、文档
---
## 0. 审查说明
本报告基于两次系统性代码审查:
1. **Go后端审查** (`internal/` 目录)
2. **React/TypeScript前端审查** (`frontend/admin/` 目录)
审查重点:安全性、并发安全、错误处理、资源管理、业务逻辑、代码规范。
---
## 1. 后端发现的问题
### 1.1 安全问题
#### [高] OAuth `ValidateToken` 无实际验证
**文件**: `internal/auth/oauth.go`, 行 433-437
```go
func (m *DefaultOAuthManager) ValidateToken(token string) (bool, error) {
// 各 provider 的 token 验证需要 provider 上下文,此处作为通用 fallback
return len(token) > 0, nil
}
```
**问题**: 接口 `OAuthManager.ValidateToken` 的实现仅检查 `len(token) > 0`,对任何非空字符串都返回 `true`。没有任何实际的 token 有效性验证逻辑。
**建议**: 移除此 fallback 实现,改为对不支持的 provider 返回明确错误,或通过各 provider 的 userinfo 端点验证 token 有效性。
---
#### [中] OAuth StateSecret 使用不安全默认值
**文件**: `internal/auth/oauth_config.go`, 行 155
```go
StateSecret: getEnv("OAUTH_STATE_SECRET", "default-secret-change-in-production"),
```
**问题**: 当环境变量 `OAUTH_STATE_SECRET` 未配置时OAuth state 的签名密钥硬编码为默认值。如果生产环境配置缺失且环境变量也未设置,攻击者可以伪造有效的 OAuth state绕过 CSRF 保护。
**建议**: 当配置缺失时显式返回错误,禁止程序以不安全的默认密钥启动。
---
#### [中] 多处类型断言缺少 ok 检查
**文件**: `internal/api/handler/auth.go`, 行 406, 567, 603, 622
多处 `userID.(int64)` 直接进行类型断言而未检查 `ok` 值。
**问题**: Gin 的 `c.Get()` 返回 `interface{}` 类型,直接进行类型断言在类型不匹配时会触发 panic。
**建议**: 添加类型检查:
```go
userIDVal, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, apierrors.New(apierrors.CodeUnauthorized, "unauthorized"))
return
}
userID, ok := userIDVal.(int64)
if !ok {
c.JSON(http.StatusUnauthorized, apierrors.New(apierrors.CodeUnauthorized, "invalid user id"))
return
}
```
---
#### [中] ResendActivationEmail 存在用户枚举风险
**文件**: `internal/api/handler/auth.go`, 行 264-277
**问题**: 服务端通过 `if the email exists...` 的提示文字,隐式地承认了邮箱是否存在。但通过观察响应是否一致来判断邮箱存在性的攻击者,可以结合响应时间侧信道来枚举有效邮箱。
**建议**: 确保整个邮件发送流程的总耗时在两种情况下保持一致(可增加人工延迟),防止时间侧信道攻击。
---
### 1.2 并发安全
#### [高] StateManager 清理 goroutine 无法停止
**文件**: `internal/auth/state.go`, 行 68-75
```go
func (sm *StateManager) StartCleanupRoutine() {
ticker := time.NewTicker(5 * time.Minute)
go func() {
for range ticker.C {
sm.Cleanup()
}
}}()
}
```
**问题**: `StartCleanupRoutine` 启动的后台 goroutine 没有任何停止机制。当程序需要优雅关闭时,无法等待或通知此 goroutine 退出,可能导致 goroutine 泄漏。
**建议**: 添加 stop channel
```go
func (sm *StateManager) StartCleanupRoutine(stop <-chan struct{}) {
ticker := time.NewTicker(5 * time.Minute)
go func() {
for {
select {
case <-ticker.C:
sm.Cleanup()
case <-stop:
ticker.Stop()
return
}
}
}()
}
```
---
#### [高] 内存 rate limiter map 无界限增长
**文件**: `internal/api/middleware/ratelimit.go`, 行 84-106
**问题**: `limiters` map 为每个不同的 IP 地址或用户 ID 创建一个 rate limiter 实例并永久存储,从不清理。随着时间推移和用户增多,该 map 会无限增长,导致内存持续消耗。
**建议**: 实施 LRU 淘汰策略或 TTL 机制,定期清理长期未使用的 limiter 条目。
---
#### [高] L1Cache 无最大容量限制
**文件**: `internal/cache/l1.go`, 行 19-46
**问题**: `L1Cache` 是一个无界的并发安全 map既没有最大条目数限制也没有 LRU/TTL 淘汰策略。
**建议**: 为 `L1Cache` 添加最大容量限制和 LRU 淘汰策略。
---
#### [中] TOTP 恢复码删除非原子
**文件**: `internal/service/totp.go`, 行 130-133
**问题**: 恢复码在内存中从 slice 中删除后,如果 `UpdateTOTP` 数据库更新失败(`err` 被忽略),该恢复码实际上已从内存中移除,但数据库中的记录并未更新——导致一个恢复码被使用两次的窗口期。
**建议**: `UpdateTOTP` 的错误不应被忽略,应回滚内存中的恢复码列表。
---
### 1.3 资源管理
#### [中] 头像上传路径处理
**文件**: `internal/api/handler/avatar.go`, 行 28
```go
avatarDir = "./uploads/avatars"
```
**问题**: 头像目录使用相对路径,可能存在路径遍历风险。
**建议**: 使用绝对路径并通过配置管理,对文件名进行 UUID 化处理。
---
#### [低] RSA 私钥文件权限设置过于宽松
**文件**: `internal/auth/jwt.go`, 行 212
```go
if err := os.WriteFile(privatePath, privatePEM, 0o644); err != nil {
```
**建议**: 将私钥文件权限改为 `0o600`
---
### 1.4 业务逻辑
#### [中] TOTP Disable 时恢复码直接清空无审计日志
**文件**: `internal/service/totp.go`, 行 81-106
**问题**: 当用户通过恢复码成功禁用 2FA 时,所有未使用的恢复码全部被删除,但没有任何记录表明"哪些恢复码被作废",无法区分是用户主动禁用还是攻击者通过恢复码禁用了账号。
**建议**: 在操作日志中记录 2FA 禁用事件(包含操作来源 IP并考虑对"通过恢复码禁用 2FA"触发安全告警。
---
#### [低] 密码强度评分对短密码过于宽松
**文件**: `internal/service/auth.go`, 行 276-298
**问题**: 当 `strict=false` 时,仅要求 `info.Score < 2` 才拒绝,即密码长度 8 位且包含任意一种字符类型Score=1也可通过验证。这意味着 `"aaaaaaaa"` 这样极弱的密码可通过验证。
**建议**: 调整评分阈值或增强评分逻辑,确保长度 8 位的单类型字符密码被拒绝。
---
### 1.5 代码规范
#### [中] `social_account_repo.go` 使用原生 SQL 而非 GORM
**文件**: `internal/repository/social_account_repo.go`
**问题**: 整个代码库使用 GORM 作为 ORM`SocialAccountRepository` 使用 `*sql.DB` 的原生 SQL 实现,绕过了 GORM 的事务管理和连接池封装。
**建议**: 将 `SocialAccountRepository` 迁移为 GORM 实现,与其他 repository 保持一致。
---
#### [中] 多处错误被静默忽略
多处 `_ = err``_ = json.Marshal(...)` 的静默错误处理:
- `internal/service/totp.go` 行 47, 94, 131, 133
- `internal/api/handler/auth.go` 行 379
- `internal/api/middleware/operation_log.go` 行 87
**问题**: 静默忽略错误使得调试困难,且可能导致数据不一致。
**建议**: 对于关键操作的错误,不应忽略,至少记录日志。
---
#### [低] 命名不一致
**文件**: `internal/auth/oauth.go`, 行 18
`OProviderGoogle`(大写 O与其他 `OAuthProviderXXX` 常量命名风格不一致。
**建议**: 统一为 `OAuthProviderGoogle`
---
## 2. 前端发现的问题
### 2.1 安全性
#### [高] `uploadAvatar` 字段名可能错误
**文件**: `frontend/admin/src/services/profile.ts`, 行 49
```typescript
export function uploadAvatar(userId: number, file: File): Promise<AvatarUploadResponse> {
const formData = new FormData()
formData.append('avatar', file) // ← 字段名可能是 'file'
return post<AvatarUploadResponse>(`/users/${userId}/avatar`, formData)
}
```
**问题**: 函数签名的第二个参数名为 `file`FormData 中使用的字段名是 `avatar`,但后端期望的字段名可能是 `file`。这会导致后端无法识别上传的文件字段。
**建议**: 确认后端期望的字段名,保持前后端一致。
---
#### [中] 操作日志字段未经 HTML 转义直接渲染
**文件**: `frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.tsx`, 行 31, 35, 45
**问题**: `request_path``request_params``user_agent` 均来自后端日志数据,如果包含 XSS 可执行脚本,在管理后台中可能造成风险。
**建议**: 使用 AntD 的 `text` 属性而非 `children` 来渲染这些用户可控字段。
---
### 2.2 状态管理
#### [中] React 状态与模块状态双重管理
**文件**: `frontend/admin/src/app/providers/AuthProvider.tsx`, 行 45-50, 127-181
**问题**: `sessionState`(模块级变量)和 React state (`user`, `roles`) 同时保存用户信息。当某处只更新了模块状态而未更新 React 状态时fallback 机制会掩盖问题。
**建议**: 统一状态管理范式,只使用 React state 作为唯一数据源。
---
#### [中] `onPressEnter` 绑定时未使用 `void`
**文件**: `frontend/admin/src/pages/admin/UsersPage/UsersPage.tsx`, 行 403
```typescript
onPressEnter={fetchUsers} // ← fetchUsers 是 async 函数
```
**问题**: `fetchUsers``async` 函数,返回 Promise`onPressEnter` 期望的是 `React.KeyboardEventHandler`
**建议**: `onPressEnter={() => void fetchUsers()}`
---
### 2.3 性能
#### [高] Webhooks 全量加载后在客户端分页,无服务端分页
**文件**: `frontend/admin/src/pages/admin/WebhooksPage/WebhooksPage.tsx`, 行 50-61, 73-82
```typescript
const fetchWebhooks = useCallback(async () => {
const result = await listWebhooks() // ← 获取全部数据
setWebhooks(result)
}, [])
```
**问题**: `listWebhooks()` 无任何参数,后端返回全部 webhook 数据。当 webhook 数量增长时,会导致网络传输大量无用数据、客户端内存占用过高、过滤和分页全在主线程执行。
**建议**: 为 `listWebhooks` 添加服务端分页支持(`page`, `page_size`)。
---
#### [中] ProfileSecurityPage 单组件管理 ~30 个状态变量
**文件**: `frontend/admin/src/pages/admin/ProfileSecurityPage/ProfileSecurityPage.tsx`, 行 72-103
**问题**: 单个组件管理超过 30 个状态变量,任何一个状态变化都会触发整个组件重新渲染。这个 880 行的巨型组件应该被拆分。
**建议**: 拆分为多个子组件AvatarSection、PasswordSection、TOTPSection、SocialBindingSection、DevicesSection、AuditLogSection。
---
### 2.4 类型安全
#### [中] `ApiResponse.data` 类型为 `T` 而非 `T | null`
**文件**: `frontend/admin/src/types/http.ts`, 行 8-15
**问题**: 某些后端 API 响应(如 204 No Content`data` 字段可能为 `null``undefined`,但类型定义为非空。
**建议**: `data: T` 改为 `data: T | null`,在访问前做空值检查。
---
### 2.5 组件设计
#### [高] ProfileSecurityPage 未复用已有的 ContactBindingsSection
**文件**: `frontend/admin/src/pages/admin/ProfileSecurityPage/ProfileSecurityPage.tsx`, 行 656-663
**问题**: `ContactBindingsSection` 组件已在同目录下定义,但 `ProfileSecurityPage` 中的邮箱/手机号绑定逻辑与 `ContactBindingsSection` 存在功能重叠和重复实现。
**建议**: 确认 `ContactBindingsSection` 的职责范围,如果它已包含完整的绑定 UI删除 `ProfileSecurityPage` 中的重复 `ContentCard`
---
## 3. 问题汇总
### 后端问题统计
| 编号 | 严重程度 | 分类 | 文件 | 行号 |
|------|----------|------|------|------|
| 1.1 | 高 | 安全 | `auth/oauth.go` | 433-437 |
| 1.2 | 中 | 安全 | `auth/oauth_config.go` | 155 |
| 1.3 | 中 | 安全 | `api/handler/auth.go` | 406, 567, 603, 622 |
| 1.4 | 中 | 安全 | `api/handler/auth.go` | 264-277 |
| 2.1 | 高 | 并发 | `auth/state.go` | 68-75 |
| 2.2 | 高 | 并发 | `api/middleware/ratelimit.go` | 84-106 |
| 2.3 | 高 | 资源 | `cache/l1.go` | 19-46 |
| 2.4 | 中 | 并发 | `service/totp.go` | 130-133 |
| 3.1 | 中 | 资源 | `api/handler/avatar.go` | 28 |
| 3.2 | 低 | 资源 | `auth/jwt.go` | 212 |
| 4.1 | 中 | 业务 | `service/totp.go` | 81-106 |
| 4.2 | 低 | 业务 | `service/auth.go` | 293 |
| 5.1 | 中 | 规范 | `repository/social_account_repo.go` | 全文件 |
| 5.2 | 中 | 规范 | 多处 | 多行 |
| 5.3 | 低 | 规范 | `auth/oauth.go` | 18 |
**后端总计**: 高危 4 个,中危 8 个,低危 3 个
### 前端问题统计
| 编号 | 严重程度 | 分类 | 文件 |
|------|----------|------|------|
| 1.1 | 高 | 安全 | `services/profile.ts` |
| 1.2 | 中 | 安全 | `OperationLogDetailDrawer.tsx` |
| 2.1 | 中 | 状态管理 | `AuthProvider.tsx` |
| 2.2 | 中 | 状态管理 | `UsersPage.tsx` |
| 3.1 | 高 | 性能 | `WebhooksPage.tsx` |
| 3.2 | 中 | 性能 | `ProfileSecurityPage.tsx` |
| 4.1 | 中 | 类型安全 | `http.ts` |
| 5.1 | 高 | 组件设计 | `ProfileSecurityPage.tsx` |
**前端总计**: 高危 3 个,中危 5 个
---
## 4. 已确认的良好实践
以下方面经审查确认为良好实践,无需修改:
### 后端
1. **JWT JTI 黑名单**: 访问令牌和刷新令牌都包含 JTI支持基于 JTI 的令牌黑名单Logout 机制完善
2. **密码哈希**: 使用 Argon2id64MB 内存3 次迭代bcrypt 作为向后兼容,均使用 `crypto/rand` 生成盐
3. **SQL 注入防护**: GORM 参数化查询,`escapeLikePattern` 正确处理 LIKE 通配符转义
4. **CSRF Token**: 使用 `crypto/rand` 生成 16 字节随机数
5. **TOTP 验证**: 使用 pquerna/otp 库,支持前后各 1 个时间窗口
6. **OAuth state 管理**: 使用带 TTL 的 in-memory map 存储 state防止 CSRF
7. **OAuth return_to 校验**: 验证 URL scheme、origin 白名单,防止开放重定向
8. **头像上传**: 内容类型白名单检查、图片尺寸解码后验证、文件大小限制
9. **敏感字段日志脱敏**: `sanitizeParams` 过滤 password、token 等敏感字段
10. **Rate Limiting**: 支持 Token Bucket、Leaky Bucket、Sliding Window 三种算法
11. **IP 过滤**: 支持 CIDR 范围、AnomalyDetector 自动封禁
12. **并发安全**: L1/L2 cache 使用 `sync.RWMutex`StateManager 使用 `sync.RWMutex`
13. **Context 超时**: 数据库操作、缓存操作均通过 `context.WithContext` 传递超时
### 前端
1. **认证状态管理**: 内存-only token不持久化到 localStorage/sessionStorage
2. **窗口守卫**: `window.alert/confirm/prompt/open` 被阻断并记录为结构化错误
3. **错误处理**: `AppError` 类封装了完整的错误类型和响应映射
4. **HTTP 客户端**: 完整的 401 自动刷新机制
5. **组件测试**: 高覆盖率的组件测试
---
## 5. 优先级修复建议
### 第一优先级(高危,必须修复)
1. OAuth `ValidateToken` fallback 实现 - 安全漏洞
2. StateManager goroutine 无法停止 - 资源泄漏
3. Rate limiter map 无界限增长 - 内存泄漏
4. L1Cache 无最大容量限制 - 内存泄漏
5. `uploadAvatar` 字段名可能错误 - 功能性 bug
6. Webhooks 全量加载无分页 - 性能和可扩展性问题
7. ProfileSecurityPage 未复用 ContactBindingsSection - 代码重复
### 第二优先级(中危,建议修复)
1. OAuth StateSecret 不安全默认值
2. 多处类型断言缺少 ok 检查
3. TOTP 恢复码删除非原子
4. 多处错误被静默忽略
5. `social_account_repo.go` 使用原生 SQL 而非 GORM
6. React 状态与模块状态双重管理
7. `onPressEnter` 绑定未使用 `void`
8. ProfileSecurityPage 单组件管理 ~30 个状态变量
9. 操作日志字段未经 HTML 转义直接渲染
### 第三优先级(低危,可选修复)
1. RSA 私钥文件权限过于宽松
2. 密码强度评分对短密码过于宽松
3. 命名不一致 (`OProviderGoogle`)
4. `ApiResponse.data` 类型定义问题
---
## 6. 文档一致性
### 发现的问题
1. **PROJECT_REVIEW_REPORT.md** - 文件编码损坏,需要重新创建为 UTF-8 编码
2. **DATA_MODEL.md** - 以下表格与实际实现不符:
- `verification_codes` - 无独立表(内存/Redis管理
- `token_blacklist` - 未实现
- `user_custom_fields` - 未实现
- `system_configs` - 通过 config.yaml 管理
- `audit_logs` - 实际表名为 `operation_logs`
---
*本报告由系统审查生成审查日期2026-03-29*