# Handler 层参数校验安全加固 - 设计文档 **功能 ID**: `handler-validation-security-001` ## 设计目标 防止参数校验错误泄露内部实现细节(validator 规则、字段名、类型信息),提升 API 安全性。 ## 问题分析 ### 当前问题 在 Handler 层中,参数解析和验证失败时,直接将底层错误信息(`err.Error()`)拼接后返回给客户端,导致以下安全风险: 1. **泄露 DTO 字段名**:`Field validation for 'Username' failed on the 'required' tag` 2. **泄露验证规则**:客户端可以知道哪些字段必填、长度限制、格式要求等 3. **泄露类型信息**:`Unmarshal type error: expected=uint got=string field=shop_id` 4. **便于反向工程**:攻击者可以根据错误信息探测 API 内部结构 ### 影响范围(基于扫描结果) ``` 总计: 32 个 handler 文件,11 处错误泄露点 Admin Handler (29 个文件) ├── auth.go (3 处) │ ├── Login() - 行 35 │ ├── RefreshToken() - 行 80 │ └── ChangePassword() - 行 133 ├── role.go (4 处) │ ├── Create() - 行 39 │ ├── Update() - 行 80 │ ├── AssignPermissions() - 行 136 │ └── RemovePermissions() - 行 197 └── storage.go (1 处) └── GenerateUploadURL() - 行 32 H5 Handler (3 个文件) └── auth.go (3 处) ├── Login() - 行 35 ├── RefreshToken() - 行 80 └── ChangePassword() - 行 133 ``` ## 设计方案 ### 核心原则 | 原则 | 说明 | |------|------| | **对外通用** | 客户端收到的错误消息不包含内部细节 | | **日志详细** | 服务端日志记录完整的错误信息用于排查 | | **一致性** | 所有 Handler 使用相同的错误处理模式 | | **安全性** | 防止通过错误消息进行探测攻击 | ### 修复策略 #### 策略 1:批量修复优先 针对已发现的 11 处错误泄露点,优先修复: ``` Phase 1: 修复已知错误点(预估 1h) ├── admin/auth.go (3 处) ├── admin/role.go (4 处) ├── admin/storage.go (1 处) └── h5/auth.go (3 处) Phase 2: 全量检查(预估 1h) └── 检查其余 28 个文件是否有类似问题 ``` #### 策略 2:使用模板替换 定义 3 种标准修复模板,确保一致性: | 场景 | 修复模板 | |------|---------| | 参数解析错误 | 模板 A | | 参数验证错误 | 模板 B | | 参数格式错误 | 模板 C | ## 技术设计 ### 错误处理流程 #### 当前流程(有安全风险) ```mermaid graph LR A[Handler 接收请求] --> B[BodyParser/Validate] B -->|失败| C[拼接 err.Error()] C --> D[返回详细错误给客户端] D --> E[❌ 泄露内部细节] ``` #### 修复后流程(安全) ```mermaid graph LR A[Handler 接收请求] --> B[BodyParser/Validate] B -->|失败| C{记录日志} C --> D[logger.Warn 记录详细错误] C --> E[返回通用错误消息] D --> F[✅ 日志包含完整信息] E --> G[✅ 客户端不泄露细节] ``` ### 修复模板 #### 模板 A:参数解析错误 ```go // ❌ 修复前 if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "参数解析失败: "+err.Error()) } // ✅ 修复后 if err := c.BodyParser(&req); err != nil { logger.GetAppLogger().Warn("参数解析失败", zap.String("path", c.Path()), zap.String("method", c.Method()), zap.Error(err), ) return response.Error(c, 400, errors.CodeInvalidParam, "参数解析失败") } ``` **关键变更**: - ✅ 添加结构化日志(path、method、error) - ✅ 移除 `err.Error()` 拼接 - ✅ 对外返回通用消息 #### 模板 B:参数验证错误 ```go // ❌ 修复前 if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error()) } // ✅ 修复后 if err := h.validator.Struct(&req); err != nil { logger.GetAppLogger().Warn("参数验证失败", zap.String("path", c.Path()), zap.String("method", c.Method()), zap.Error(err), ) return errors.New(errors.CodeInvalidParam) // 使用默认 msg:"参数验证失败" } ``` **关键变更**: - ✅ 使用 `errors.New(CodeInvalidParam)` 不传自定义消息 - ✅ 自动使用 errorMessages 映射表中的默认消息 - ✅ validator 详细错误仅记录到日志 #### 模板 C:参数格式错误 ```go // ❌ 修复前 page, err := strconv.Atoi(c.Query("page", "1")) if err != nil { return response.Error(c, 400, errors.CodeInvalidParam, "页码格式错误: "+err.Error()) } // ✅ 修复后 page, err := strconv.Atoi(c.Query("page", "1")) if err != nil { logger.GetAppLogger().Warn("页码参数格式错误", zap.String("path", c.Path()), zap.String("page", c.Query("page")), zap.Error(err), ) return response.Error(c, 400, errors.CodeInvalidParam, "页码格式错误") } ``` **关键变更**: - ✅ 日志记录原始参数值(用于排查) - ✅ 移除错误细节(如 `strconv.Atoi: parsing "abc": invalid syntax`) ### 日志记录设计 #### 日志级别 | 场景 | 级别 | 原因 | |------|------|------| | 参数解析错误 | `WARN` | 客户端错误,需要记录但不是系统故障 | | 参数验证错误 | `WARN` | 客户端错误,需要记录但不是系统故障 | | 参数格式错误 | `WARN` | 客户端错误,需要记录但不是系统故障 | #### 日志字段 | 字段 | 类型 | 说明 | 示例 | |------|------|------|------| | `level` | string | 日志级别 | `"warn"` | | `ts` | string | 时间戳 | `"2026-01-30T10:00:00Z"` | | `msg` | string | 日志消息 | `"参数验证失败"` | | `path` | string | 请求路径 | `"/api/admin/accounts"` | | `method` | string | HTTP 方法 | `"POST"` | | `error` | string | 详细错误 | `"Field validation for 'Username' failed on the 'required' tag"` | #### 示例日志输出 ```json { "level": "warn", "ts": "2026-01-30T10:15:23.456Z", "msg": "参数验证失败", "path": "/api/admin/accounts", "method": "POST", "error": "Key: 'CreateAccountRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag" } ``` ### 错误响应设计 #### 修复前(泄露细节) ```json { "code": 10001, "msg": "参数验证失败: Field validation for 'Username' failed on the 'required' tag", "data": null, "timestamp": "2026-01-30T10:15:23Z" } ``` **问题**: - ❌ 泄露字段名 `Username` - ❌ 泄露验证规则 `required` - ❌ 泄露 DTO 结构 `CreateAccountRequest` #### 修复后(安全) ```json { "code": 10001, "msg": "参数验证失败", "data": null, "timestamp": "2026-01-30T10:15:23Z" } ``` **改进**: - ✅ 通用错误消息 - ✅ 不泄露内部结构 - ✅ 详细信息在服务端日志 ## 执行计划 ### Phase 1: 修复已知错误点(优先级:🔴 高) **工作量**: 1 小时 | 文件 | 错误数 | 修复内容 | |------|-------|---------| | `admin/auth.go` | 3 | 使用模板 B 修复 3 处参数验证错误 | | `admin/role.go` | 4 | 使用模板 B 修复 4 处参数验证错误 | | `admin/storage.go` | 1 | 检查并修复错误处理(可能需要自定义) | | `h5/auth.go` | 3 | 使用模板 B 修复 3 处参数验证错误 | **验证步骤**: 1. 每修复一个文件,运行 `go build -o /tmp/test_api ./cmd/api` 2. 使用 `grep` 确认该文件不再包含 `err.Error()` 拼接 ### Phase 2: 全量检查(优先级:🟡 中) **工作量**: 1 小时 检查其余 28 个 handler 文件: - 搜索所有 `BodyParser`、`QueryParser`、`Validate` 调用 - 确认错误处理符合模板 A、B、C - 发现问题立即修复 **自动化脚本**: ```bash # 检查所有可能的参数校验点 grep -n "BodyParser\|QueryParser\|validator.Struct" internal/handler/admin/*.go internal/handler/h5/*.go ``` ### Phase 3: 测试验证(优先级:🔴 高) **工作量**: 1 小时 1. **集成测试**:补充参数校验失败的测试用例 2. **手动测试**:发送错误参数验证响应格式 3. **日志验证**:确认日志包含完整错误信息 ### Phase 4: 文档更新(优先级:🟡 中) **工作量**: 0.5 小时 1. 更新 `openspec/specs/error-handling/spec.md` 2. 更新 `docs/003-error-handling/使用指南.md` ## 影响评估 ### 对外 API 影响 | 影响点 | 变更内容 | Breaking Change | |--------|---------|-----------------| | 错误消息 | 从详细错误变为通用消息 | ✅ 是 | | 错误码 | 不变(仍为 10001) | ❌ 否 | | HTTP 状态码 | 不变(仍为 400) | ❌ 否 | | 响应格式 | 不变(仍为 {code, msg, data, timestamp}) | ❌ 否 | ### 客户端适配建议 ```javascript // 前端错误处理建议 if (response.code === 10001) { // ❌ 旧方式:依赖 msg 中的字段名提示 // message.error(response.msg); // "参数验证失败: Field validation for 'Username' failed" // ✅ 新方式:使用通用提示或前端验证 message.error('请检查输入参数是否完整和正确'); // 或者依赖前端表单验证提前拦截 } ``` ### 安全性提升 | 风险 | 修复前 | 修复后 | |------|-------|-------| | 字段名泄露 | ✅ 存在 | ❌ 已消除 | | 验证规则泄露 | ✅ 存在 | ❌ 已消除 | | 类型信息泄露 | ✅ 存在 | ❌ 已消除 | | DTO 结构泄露 | ✅ 存在 | ❌ 已消除 | | 探测攻击风险 | 🔴 高 | 🟢 低 | ### 性能影响 | 指标 | 影响 | 说明 | |------|------|------| | 响应时间 | ≈ 0 | 仅增加日志写入(异步) | | 内存占用 | +0.1% | 日志缓冲区占用可忽略 | | CPU 占用 | +0.1% | 日志序列化开销可忽略 | | 磁盘占用 | +10MB/天 | WARN 级别日志增量(自动轮转) | **结论**:性能影响可忽略,安全性显著提升。 ## 后续优化 ### 可选优化方向 1. **国际化错误消息**: - 当前返回中文错误消息 - 可根据 `Accept-Language` 返回多语言错误 - 需要扩展 `errorMessages` 映射表 2. **错误码细化**: - 当前所有参数错误都是 `10001` - 可细化为:`10001` 参数缺失、`10002` 参数格式错误、`10003` 参数值非法 - 便于前端差异化处理 3. **错误追踪**: - 在响应中添加 `request_id` 字段 - 客户端可通过 request_id 联系客服定位问题 - 需修改 `response.Error()` 函数 ## 验证清单 - [ ] 所有 11 处错误泄露点已修复 - [ ] 所有 Handler 文件检查完毕 - [ ] `grep -r "err\.Error()" internal/handler/` 无残留(除日志外) - [ ] 编译通过 `go build -o /tmp/test_api ./cmd/api` - [ ] 集成测试通过 - [ ] 手动测试验证不泄露字段名 - [ ] 日志包含完整错误信息 - [ ] 文档已更新 - [ ] Code Review 通过 ## 参考资料 - [OWASP - Information Leakage](https://owasp.org/www-community/vulnerabilities/Information_Leakage) - [项目错误处理规范](../../../openspec/specs/error-handling/spec.md) - [AGENTS.md 错误报错规范](../../../AGENTS.md#错误报错规范必须遵守)