安全缺陷的修复成本随发现阶段递增:性能问题可以延后调整,泄露的数据无法收回,被入侵系统的信任损失同样难以恢复。安全控制应前移到编码与代码评审阶段;上线后补充的安全需求,多数是对已发生事故的补救。

Go 标准库提供了较好的安全默认值:html/template 默认转义,database/sql 天然支持参数化查询,Go 1.24 引入的 os.Root 可将文件访问限制在指定根目录内。但常见错误仍集中于三类写法:fmt.Sprintf 拼接 SQL、exec.Command("sh", "-c", ...)、密钥写成常量——它们在代码评审中出现频率最高。

评估每个模块前,应依次确认三个问题:不可信数据从哪里进入(HTTP 请求、文件上传、环境变量,以及其他服务写入数据库的行);流向哪些敏感操作(SQL、shell、文件路径、加密);防线被突破后影响范围有多大。下文按注入、文件、密钥、密码学、HTTP、日志、依赖的顺序展开,文末为上线前检查清单。

威胁建模

**所有输入默认不可信,校验应集中在信任边界进行。**中间件解析过的参数、上游服务处理过的数据,到达下一个边界(另一个服务或进程)时都应重新校验,并附加长度上限。在边界处拦截非法输入,内部代码才能保持简单。

**校验应使用白名单,而非黑名单。**黑名单必然存在遗漏:拦截 ../ 之后,仍有 ..\、URL 编码等变体;枚举"允许什么"天然完备,枚举"禁止什么"天然残缺。

纵深防御的另一项要求是:上游有校验不豁免本层,风险等级降低不等于可以忽略;上游防线被移除或绕过时,本层即最后一道防线。对放行位置应留下注释,便于审计追溯:

// security: 此处拼接安全——col 来自白名单映射,而非用户原始输入

STRIDE 六类威胁(伪装、篡改、抵赖、信息泄露、拒绝服务、提权)应在每个信任边界逐项检查,再用 DREAD 打分排定优先级:810 分的 Critical 项(RCE、全量数据泄露、凭证被盗)应立即修复,67.9 分的项应在当前迭代内完成。

最小权限原则应贯穿始终:调试端口仅绑定 127.0.0.1,服务账号仅授予所需数据表的权限。临时放宽的权限会扩大攻击面,为攻击者的横向移动提供路径。

注入

**SQL 应一律参数化,用户输入不得进入字符串拼接。**参数化查询将数据与指令分离,输入不会被解释为 SQL 语法:

// ❌ 单引号就能改写查询语义
q := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", input)

// ✅ 参数化(PostgreSQL 用 $1,MySQL 用 ?)
err := db.QueryRow("SELECT * FROM users WHERE name = $1", input).Scan(&name)

动态 IN 子句应展开为 $1, $2, ... 占位符(或用 sqlx.In),不应以 strings.Join 拼接用户字符串;多条件查询应用构建器逐条追加 WHERE 与参数,详见数据库访问规范。

占位符仅适用于值。列名、表名、ORDER BY 等标识符无法参数化,必须经白名单映射:

var allowed = map[string]string{"name": "name", "created": "created_at"}

col, ok := allowed[sortCol]
if !ok {
    col = "created_at" // 未命中时回退到默认列
}
q := "SELECT * FROM users ORDER BY " + col // 安全:col 来自白名单

外部命令应用 exec.Command 分离参数,禁止经 shell 解释。sh -c 会把 ;、|、反引号当作元字符执行,接入外部输入即构成 RCE;文件名应先约束为纯文件名:

// ❌ 经过 shell 解释:分号后的命令会被执行
exec.Command("sh", "-c", "rm -f /tmp/"+filename)

// ✅ 参数分离避免 shell 解释,文件名另行校验
if filepath.Base(filename) != filename {
    return errors.New("invalid filename")
}
exec.Command("rm", "-f", filepath.Join("/tmp", filename))

HTML 输出应交给 html/template,不应手动拼接标签——fmt.Fprintf(w, "<div>%s</div>", input) 是典型的 XSS 注入入口。模板引擎按上下文转义,<script> 只会呈现为无害文本:

t := template.Must(template.New("t").Parse("<div>{{.}}</div>"))
t.Execute(w, input) // 自动转义

用户提供的 URL 应在 http.Get 或 http.Redirect 之前先 url.Parse:协议只放行 http/https,域名必须在白名单内,并拒绝内网地址与 metadata. 端点,否则攻击者可借该服务访问内网;SSRF 与开放重定向均按此防范。

反序列化不应使用 gob:它未针对对抗性输入加固,解码不可信数据有触发任意构造逻辑的风险。encoding/json 不执行代码,但解码出的结构体在业务使用前仍应逐字段校验。

文件路径与临时文件

用户可控路径在 Go 1.24+ 应首选 os.Root:所有操作被限定在根目录内,越界路径与逃逸符号链接会被直接拒绝:

root, err := os.OpenRoot("/var/www")
if err != nil { return err }
defer root.Close()
f, err := root.Open(filename) // 路径被限制在根目录内

filepath.Clean 加 strings.HasPrefix 判前缀并不可靠:/base-evil 这类同前缀目录与符号链接均可绕过。旧版本可将 filepath.IsLocal 作为最终防线——先拒绝绝对路径,再用 filepath.Rel 确认结果仍在 base 之下;但词法校验防不住符号链接,应优先升级到 os.Root。

zip 解压应逐条检查条目名,非本地路径一律拒绝:恶意压缩包中的 ../../x.sh 经 filepath.Join 拼接后即可写出根目录,该问题称为 ZipSlip。解压与拷贝应限制总量,几 MB 的压缩包可解压出数 GB 数据,足以耗尽磁盘空间。

临时文件应使用 os.CreateTemp,避免可预测文件名——固定文件名在多用户环境中可能被抢先创建并写入:

f, err := os.CreateTemp("", "myapp-*") // 随机文件名
if err != nil { return err }
defer os.Remove(f.Name())
f.Chmod(0600) // 收紧到仅属主可读写

密钥管理

**密钥不得写入源码——常量、默认值、DSN 连接串均属此类。**秘密一旦提交即应按已泄露处理:它会留在版本历史、CI 日志与备份中,撤销与轮换成本极高:

// ❌ 这两行一旦提交,就得当作已泄露处理
const jwtSecret = "my-super-secret-jwt-key"
dsn := "user:Password123!@tcp(localhost:3306)/db"

// ✅ 环境变量注入,缺失即启动失败
secret := os.Getenv("JWT_SECRET")
if secret == "" {
    return nil, errors.New("JWT_SECRET is required")
}

环境变量只是下限。生产环境应使用 secret manager(AWS Secrets Manager、Vault 一类),其密钥轮换与审计能力开箱即用;staging 与生产不得共用密钥——staging 被攻破时不应波及生产环境。

.gitignore 应在模板仓库中预先配置:

.env
.env.*.local
*.key
*.pem
secrets/

密码学

**密码哈希应使用 Argon2id(首选)或 bcrypt,禁用 MD5、SHA1、单轮 SHA-256。**快速哈希适用于数据完整性校验:GPU 每秒可穷举数十亿次,密码哈希算法必须足够缓慢且内存困难:

// ❌ 计算过快,无法抵抗暴力破解
hash := sha256.Sum256([]byte(password))

// ✅ Argon2id:慢且内存困难
key := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)

// ✅ bcrypt 自动处理 salt
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

**安全随机数必须使用 crypto/rand。**session ID、token、salt、nonce 等所有安全相关场景均属此类;math/rand 的输出是可预测的:

buf := make([]byte, 32)
_, err := rand.Read(buf) // crypto/rand,而非 math/rand

**对称加密应使用 AES-256-GCM 或 ChaCha20-Poly1305,nonce 绝不复用。**GCM 下复用 nonce 会同时破坏机密性与认证性,属于 Critical 级错误;ECB 模式则将明文结构直接保留在密文中。nonce 应每次随机生成、前置存储:

block, _ := aes.NewCipher(key)
aead, _ := cipher.NewGCM(block)
nonce := make([]byte, aead.NonceSize()) // 96-bit
if _, err := rand.Read(nonce); err != nil { return err }
ct := aead.Seal(nonce, nonce, plaintext, nil) // nonce 前置存储

比较密钥、token、MAC 应用 crypto/subtle.ConstantTimeCompare 或 hmac.Equal,不应使用 ==——它在第一个不同字节处短路,耗时差异足以让攻击者逐字节猜出密钥:

ok := subtle.ConstantTimeCompare([]byte(input), []byte(expected)) == 1

TLS 最低 1.2,证书校验不得关闭:InsecureSkipVerify: true 等同于放任中间人攻击。加密出错必须 fail closed——_, _ = encrypt(data) 这类忽略错误的写法,会让损坏的密文照常进入后续流程。DES、RC4、MD5、SHA1 只应出现在禁用清单中。

HTTP 安全

会话 Cookie 应设置 Secure、HttpOnly、SameSite=Strict/Lax,Domain 留空、MaxAge 从短。缺少任一属性都会扩大攻击面:没有 HttpOnly,JavaScript 可经 document.cookie 读取会话;没有 SameSite,跨站请求会自动携带 Cookie。

http.SetCookie(w, &http.Cookie{
    Name: "session", Value: token,
    Secure:   true,                 // 仅 HTTPS 发送
    HttpOnly: true,                 // JS 不可读
    SameSite: http.SameSiteLaxMode, // 多数应用选 Lax,高敏感用 Strict
    Path: "/", MaxAge: 3600,
    Domain: "", // 默认只发给当前主机
})

仅依赖 SameSite 不足以防御 CSRF,状态变更请求还应配合 double-submit cookie 或 CSRF token。授权检查只认服务端:HTTP 客户端不执行 JavaScript,前端隐藏按钮不构成权限控制。

CSP、HSTS、X-Frame-Options、nosniff 等安全响应头应由中间件统一设置。CSP 将资源来源限制到 'self',XSS 即便发生也难以外带数据;CORS 同理,只对确需跨域的来源开放:

func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        h := w.Header()
        h.Set("Content-Security-Policy", "default-src 'self'")
        h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
        h.Set("X-Frame-Options", "DENY")
        h.Set("X-Content-Type-Options", "nosniff")
        next.ServeHTTP(w, r)
    })
}

http.Server 必须配置超时与大小上限。缺省配置下,Slowloris 类慢速连接会长期占用连接;请求体不设上限,最终将耗尽内存:

srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
// 请求体用 http.MaxBytesReader 限流;端点限流用 golang.org/x/time/rate

_ "net/http/pprof" 会把运行时信息暴露在公开路由上,此类调试端点要么不进入生产,要么绑定 127.0.0.1 的独立 listener;服务只应监听必要网卡,不应绑定 0.0.0.0。

日志与错误

**密码、token、邮箱等 PII 不得进入日志。**日志会被采集、存储并流转至第三方平台,每一环节都是潜在泄露面:

// ❌ 整个结构体直接输出,密码和 token 全在里面
log.Printf("login: %+v", user)

// ✅ 只记必要字段
logger.Info("user_login", "user_id", user.ID)

用户输入进入日志前应过滤控制字符:伪造换行可插入虚假条目,干扰采集与告警规则。

对客户端应返回通用错误,细节写入服务端日志;堆栈、连接串、SQL 方言组合起来可为攻击者提供系统内部结构:

// ❌ 内部细节直达客户端
http.Error(w, "database error: "+err.Error(), http.StatusInternalServerError)

// ✅
logger.Error("db_error", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)

接入 Sentry 等分析类第三方服务时,应用 BeforeSend 钩子与字段过滤层在上报前剔除 Authorization、Cookie 等敏感头与 PII,上报字段只保留内部 ID 与非识别性业务字段;确需关联用户时对 PII 做哈希。

依赖与工具链

CI 中应常开三项:govulncheck 扫描依赖的已知 CVE(只报实际调用链可达的)、gosec 做 SAST、go test -race 检测数据竞争。race 告警不应按低危处理——数据竞争不仅损坏数据,还可能使授权检查在并发下被绕过:

go get -tool github.com/securego/gosec/v2/cmd/gosec@latest
go tool gosec ./...        # SAST:硬编码、弱加密、路径拼接
go tool govulncheck ./...  # 只报实际调用链可达的 CVE
go test -race ./...

依赖应定期升级,未打补丁的依赖长期是第一大攻击向量。govulncheck 完整用法、依赖收敛与审计流程见依赖管理规范与CI/CD 规范;扫描应在流水线中自动执行,而非在审计时手工补做。

速查清单

输入与注入

  • 输入应仅在信任边界校验:白名单加长度上限
  • SQL 应全部参数化;ORDER BY 等标识符应经白名单映射
  • 外部命令应分离参数,不得使用 sh -c 拼接
  • HTML 输出应经 html/template;出站 URL 与重定向应走白名单,并屏蔽 metadata 地址
  • 不应使用 gob 处理不可信数据;JSON 解码后应逐字段校验

文件系统

  • 用户路径应限制在根目录内(Go 1.24+ 使用 os.Root),不应以 Clean+HasPrefix 判断
  • zip 解压应逐条校验条目名;解压与拷贝应限制总量
  • 临时文件应使用 os.CreateTemp 并设 0600 权限,目录 0750

密钥与密码学

  • 不得硬编码密钥;.gitignore 应排除 .env、.key、.pem;密钥应按环境隔离
  • 密码应使用 Argon2id 或 bcrypt;随机数应仅使用 crypto/rand
  • 对称加密应使用 AES-GCM;nonce 应每次随机生成且不复用
  • 密钥比较应使用 crypto/subtle;TLS ≥ 1.2,禁用 InsecureSkipVerify
  • 加密出错应 fail closed

HTTP

  • Cookie 应设置 Secure、HttpOnly、SameSite
  • 状态变更请求应具备 CSRF 防护;授权检查应仅在服务端执行
  • 应以中间件统一设置 CSP、HSTS、X-Frame-Options、nosniff
  • 超时与请求体上限应齐备;认证端点应限流
  • pprof 不应对外公开;不应绑定 0.0.0.0

日志与依赖

  • 日志应无 PII、无密钥;用户输入入日志前应过滤控制字符
  • 客户端错误响应应保持通用
  • 第三方上报应经 BeforeSend 或过滤层脱敏
  • CI 应运行 govulncheck、gosec、-race;依赖应定期升级