database/sql本身不防注入,安全取决于是否将用户输入作为参数传入:值必须用驱动匹配的占位符(MySQL用?,PostgreSQL用$1),结构如表名、字段名、ORDER BY等须白名单校验;IN子句需动态生成占位符,不可直接传切片。

database/sql 本身不防注入,安全与否完全取决于你是否把用户输入当参数传进去——Go 1.22 没引入新机制,老规则依然铁律:值走占位符,结构走白名单。
占位符必须严格匹配驱动,写错等于裸奔
Go 标准库 database/sql 不定义占位符语法,全看驱动实现。混用或写错不会编译报错,但会静默失效:
- MySQL 驱动(
github.com/go-sql-driver/mysql)只认?,写$1会报pq: syntax error at or near "?"或更隐蔽的sql: expected 0 arguments, got 1 - PostgreSQL 驱动(
github.com/lib/pq)只认$1、$2,写?直接 panic - SQLite 驱动(
github.com/mattn/go-sqlite3)虽支持多种写法,但推荐统一用?,避免跨环境行为不一致 - 常见翻车:
db.Query("SELECT * FROM users WHERE id = $1", 123)在 MySQL 下查不到数据——$1被当字面量,不是参数绑定
IN (?) 不支持多值展开,手动拼占位符是唯一正解
MySQL 驱动不支持 IN (?) 自动展开成 IN (?, ?, ?),GORM 的 Where("id IN ?", []int{1,2,3}) 能自动展开,但原生 database/sql 不行:
- 错误写法:
db.Query("SELECT * FROM users WHERE id IN (?)", ids)→ 只绑定一个参数,ids被转成字符串"[1 2 3]",查询失败 - 正确做法:动态生成占位符串,再传参:
placeholders := strings.Repeat("?,", len(ids)-1) + "?",然后db.Query("SELECT * FROM users WHERE id IN ("+placeholders+")", ids...) - 注意:
ids...必须是切片,且长度与placeholders中?数量严格一致,否则报sql: expected X arguments, got Y
表名、字段名、ORDER BY 这些地方不能用占位符,白名单校验绕不开
database/sql 协议层根本不允许参数化标识符(identifier),试图塞 ? 进去只会 panic 或被数据库直接拒绝:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
db.Query("SELECT * FROM ?", tableName)→ panic: sql: expected 0 arguments, got 1 -
db.Query("SELECT * FROM users ORDER BY ?", sortField)→ 多数驱动返回语法错误或空结果 - 排序字段白名单示例:
validSort := map[string]bool{"name": true, "created_at": true, "score": true},查不到就拒掉 - 排序方向必须校验:
if dir != "ASC" && dir != "DESC" { return errors.New("invalid sort direction") },别信strings.ToUpper(dir)后直接拼 - 分表逻辑别拼:
"logs_" + tenantID是高危操作;改用switch tenantID { case "prod": table = "logs_prod" }
Prepare 后忘记 Close 会泄漏连接,但 Query/Exec 内部已自动处理
*sql.Stmt 是有状态对象,持有底层连接资源。高频复用时需显式管理,但日常单次查询不用硬套 Prepare:
- 必须配对:
defer stmt.Close(),尤其在函数内多次调用时;漏掉会导致连接池耗尽,现象是sql: connection refused或长时间阻塞 - 别在循环里反复
Prepare同一语句——应复用*sql.Stmt,它本身并发安全 - 如果语句只用一次,直接用
db.Query或db.Exec更稳妥,它们内部会自动Prepare + Close,没性能损失,也无泄漏风险 - Go 1.22 对连接池和预处理重试逻辑没重大变更,旧版驱动(如某些 go-sql-driver/mysql 旧版本)在连接断开后未自动重准备的问题依然存在,显式
Prepare并非“更安全”,只是为性能优化
最易被忽略的一点:日志里打印 SQL 时,如果原始 SQL 是拼出来的,那日志系统就成了注入入口——哪怕你本地没出问题,远程日志服务可能被恶意 payload 触发解析漏洞。真正该打的日志是参数化模板 + 脱敏参数值,而不是原始字符串。










