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被篡改
错误码分段安全架构 模块标识 (XX) 安全等级 (YY) 具体错误 (ZZZZ) 模块示例 01: 用户模块 02: 订单模块 03: 支付模块 04: 鉴权模块 安全等级示例 00: 公开信息 10: 低敏感 20: 中敏感 30: 高敏感 具体错误示例 0001: 格式错误 0002: 次数超限 0101: Token过期 0102: Token篡改 安全断言:高敏感错误码(30级)必须触发日志脱敏 + 告警 + 限流 审计规则:禁止将高敏感错误码直接返回给客户端,必须映射为通用错误

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、时间戳、脱敏后的用户标识
错误码断言审计流程 1. 解析错误码 2. 检查分段 3. 判定敏感等级 4. 检查消息脱敏 5. 验证全局唯一 6. 审计日志记录 输出:通过 / 不通过(附违规项清单)

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
)