3. 错误码设计原则:安全审计基线
在Go服务端审计中,错误码是攻击者与防御者共同读取的“接口语言”。设计不良的错误码会直接导致信息泄露、枚举攻击、业务逻辑绕过等安全风险。本章从HTTP状态码类比出发,建立业务错误码的分段、可读性与安全断言基线。
3.1 HTTP状态码类比:错误码的语义分层
HTTP状态码的分层语义(1xx~5xx)为业务错误码提供了成熟的安全参考模型。Go服务应遵循类似的语义隔离原则,避免将传输层语义与业务层语义混为一谈。
| HTTP 状态码范围 | 语义 | 业务错误码类比 | 安全约束 |
|---|---|---|---|
| 2xx | 成功 | 0 / SUCCESS | 必须不携带敏感数据 |
| 4xx | 客户端错误 | 10000~19999 | 必须区分“不存在”与“无权限” |
| 5xx | 服务端错误 | 50000~59999 | 必须隐藏内部堆栈与数据库细节 |
| 3xx | 重定向 | 30000~39999 | 必须校验重定向目标白名单 |
审计要点: 检查Go代码中是否将HTTP 4xx与业务错误码直接映射。例如,
http.StatusNotFound 不应直接对应“用户不存在”错误码,否则攻击者可通过错误码差异进行用户枚举。
3.2 业务错误码设计:安全分段规则
业务错误码必须按模块、严重级别、安全敏感度进行分段。Go审计基线推荐使用 8位数字分段法,前两位标识模块,中间两位标识安全等级,后四位标识具体错误。
// 错误码分段结构
// XX YY ZZZZ
// ├─ 模块标识 (00~99)
// │ ├─ 01: 用户模块
// │ ├─ 02: 订单模块
// │ ├─ 03: 支付模块
// │ └─ 04: 鉴权模块
// ├─ 安全等级 (00~99)
// │ ├─ 00: 公开信息
// │ ├─ 10: 低敏感
// │ ├─ 20: 中敏感
// │ └─ 30: 高敏感(需日志脱敏)
// └─ 具体错误编号 (0000~9999)
安全分段示例:
01 00 0001— 用户模块·公开·用户名格式错误01 30 0002— 用户模块·高敏感·密码错误次数超限04 20 0101— 鉴权模块·中敏感·Token过期04 30 0102— 鉴权模块·高敏感·Token被篡改
3.3 错误码可读性要求:安全与易用的平衡
错误码必须同时满足机器可解析与人类可理解。Go审计基线要求错误码具备以下可读性特征:
- 数字唯一性: 每个错误码在全局范围内唯一,不允许复用或重载。
- 语义前缀: 错误消息必须包含错误码前缀,例如
[E01001] 用户名格式错误。 - 禁止敏感信息: 错误消息中不得包含数据库表名、字段名、IP地址、堆栈路径。
- 国际化友好: 错误码与错误消息分离,消息可替换为多语言,但错误码不变。
// 符合审计基线的错误码定义
type ErrorCode int
const (
// 用户模块 - 公开信息
ErrUsernameFormat ErrorCode = 0100001 // [E0100001] 用户名格式错误
ErrEmailFormat ErrorCode = 0100002 // [E0100002] 邮箱格式错误
// 用户模块 - 高敏感
ErrPasswordAttempts ErrorCode = 0130001 // [E0130001] 密码错误次数超限
ErrAccountLocked ErrorCode = 0130002 // [E0130002] 账户已被锁定
// 鉴权模块 - 中敏感
ErrTokenExpired ErrorCode = 0420101 // [E0420101] Token已过期
ErrTokenInvalid ErrorCode = 0420102 // [E0420102] Token无效
)
// 错误消息映射(安全脱敏)
var errorMessages = map[ErrorCode]string{
ErrUsernameFormat: "用户名格式错误",
ErrPasswordAttempts: "认证失败,请稍后重试", // 脱敏:不暴露具体次数
ErrTokenExpired: "会话已过期,请重新登录",
}
安全红线: 禁止在错误消息中拼接用户输入。例如
fmt.Sprintf("用户 %s 不存在", inputUsername) 会直接暴露用户名枚举结果。必须使用固定消息模板。
3.4 错误码断言审计清单
在Go代码审计中,针对错误码的检查应覆盖以下维度:
| 审计项 | 检查内容 | 违规示例 | 修复要求 |
|---|---|---|---|
| 分段合规 | 错误码是否按模块+安全等级分段 | 所有错误码使用连续数字 1001~1099 | 按8位分段法重构 |
| 敏感等级 | 高敏感错误码是否被返回给客户端 | 返回“密码错误次数:5” | 映射为通用错误消息 |
| 消息脱敏 | 错误消息是否包含用户输入或内部信息 | “用户 admin 不存在” | 使用固定模板 |
| 全局唯一 | 是否存在重复或冲突的错误码 | 两个模块共用 10001 | 分配独立模块段 |
| 日志记录 | 高敏感错误码是否记录完整上下文 | 仅记录错误码,无请求ID | 增加请求ID、时间戳、脱敏后的用户标识 |
3.5 常见违规模式与修复
违规模式1:错误码硬编码在业务逻辑中
// ❌ 违规:错误码散落在各处,无法统一审计
if err != nil {
return 10001, "用户不存在"
}
// ✅ 修复:使用常量定义,集中管理
if errors.Is(err, ErrUserNotFound) {
return ErrUsernameFormat, errorMessages[ErrUsernameFormat]
}
违规模式2:高敏感错误码直接暴露
// ❌ 违规:返回具体错误次数
return 0130001, fmt.Sprintf("密码错误,还剩 %d 次机会", remainingAttempts)
// ✅ 修复:脱敏为通用消息
return 0130001, "认证失败,请稍后重试"
违规模式3:错误码未分段,导致安全等级混淆
// ❌ 违规:所有错误码混用
const (
ErrUserNotFound = 1001
ErrDBTimeout = 1002 // 服务端错误与客户端错误同级
)
// ✅ 修复:按分段规则隔离
const (
// 用户模块 - 公开
ErrUserNotFound = 0100001
// 数据库模块 - 服务端错误
ErrDBTimeout = 0550001
)