使用 database/sql 时必须用占位符参数化查询,禁止字符串拼接;order by 和表名等标识符需白名单校验;scan 须处理 null 和类型匹配;日志和错误信息不得暴露敏感数据。

用 database/sql 的 Query 和 Exec 时必须传参,别拼字符串
Go 原生 database/sql 包本身不防注入,但它强制你用占位符——只要你不手动拼接 SQL 字符串,基本就安全了。很多人栽在“图省事”,比如写成 "SELECT * FROM users WHERE id = " + userID,这等于把门敞开。
- PostgreSQL/MySQL 驱动(如
lib/pq、go-sql-driver/mysql)只认$1、$2(PostgreSQL)或?(MySQL)占位符,不支持命名参数(如:name),硬写会报错:sql: expected 0 arguments, got 1 - 参数类型要匹配:传
int64给WHERE id = ?没问题,但传string表示数字 ID 时,驱动通常能隐式转换;可一旦涉及时间、JSON 或空值,隐式转换可能失败或绕过校验 - 切忌在
ORDER BY或TABLE NAME里用参数——SQL 占位符只适用于值,不适用于标识符。这类场景必须白名单校验 + 显式映射
处理动态字段名或表名时,必须用白名单 + 显式映射
想按用户选的字段排序?想查不同租户的表?这时候不能靠参数化,得靠代码逻辑兜底。否则一上 ORDER BY ?,驱动直接报错,而有人就改用 fmt.Sprintf("ORDER BY %s", input),这就崩了。
- 只允许从预设集合中选:比如排序字段限定为
[]string{"created_at", "name", "score"},用map[string]bool{"created_at": true, "name": true}快速判断是否合法 - 表名映射建议用常量+switch:比如用户输入
"prod"→ 实际查"users_prod",而不是拼"users_" + input - 别信
strings.ReplaceAll过滤单引号或分号——攻击者用注释符/**/、Unicode 空格、或换行就能绕过
用 Scan 接收结果时,注意类型和 NULL 兼容性
防注入不只是“发出去”的事,接收端出问题也可能导致逻辑异常甚至间接泄露。比如把数据库里的 NULL 直接扫进 string 变量,会 panic:sql: Scan error on column index 0: unsupported Scan, storing driver.Value type <nil> into type *string</nil>。
- 所有可能为 NULL 的字段,都该用指针类型或
sql.NullXXX类型接收,例如sql.NullString、sql.NullInt64 - 扫描到结构体时,字段名必须和 SELECT 列顺序/别名严格一致;用
SELECT id, name AS username就得对应结构体字段Username string,否则扫错位置,可能把恶意构造的字段名当数据用 - 如果用 ORM(如
gorm),注意它默认开启PrepareStmt,但若关掉(PrepareStmt: false),某些查询会退化为字符串拼接,失去参数化保护
日志和错误信息里别暴露原始 SQL 或参数值
开发时习惯打日志:log.Printf("query: %s, args: %v", query, args),上线后这就成了攻击者的探针。哪怕 SQL 是参数化的,把参数值全打出来,等于把密码、手机号、身份证号直接写进日志文件。
- 记录 SQL 时只留模板,比如
"SELECT * FROM users WHERE id = ? AND status = ?",别带实际参数 - 错误信息别直接返回给前端:
db.Query(...)失败时,err.Error()可能含敏感字段名或约束名,应统一转成"database error"并单独告警 - 用
context.WithValue传 traceID 而非拼 SQL 日志,既可追踪又不污染数据流
真正难的不是写对那几行 db.Query("SELECT ... WHERE id = ?", id),而是所有动态拼接点——哪怕只有一处 fmt.Sprintf 落在 SQL 路径里,整条链路就失效了。别依赖“我这个字段肯定不会被用户控制”,控制权永远在输入边界上,不在中间逻辑里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











