本文介绍了通过驱动程序go-mssqldb连接SQL Server的Go应用的安全实践。 它涉及 SQL 注入预防、凭证管理、加密配置以及最小权限原则。
防止 SQL 注入
SQL注入是最常见的数据库安全漏洞。 驱动程序 go-mssqldb 提供参数化查询,将 SQL 代码与数据值分离。 始终使用参数来处理用户输入。
使用参数化查询
参数与SQL文本分开发送,因此用户输入永远不会被解读为SQL代码:
// CORRECT: Parameters are sent separately from the SQL text.
rows, err := db.QueryContext(ctx,
"SELECT * FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @loc",
sql.Named("name", userName),
sql.Named("loc", userLocation))
切勿将用户输入串接到 SQL 中
字符串串接允许攻击者注入任意 SQL:
// WRONG: SQL injection vulnerability.
query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE FirstName = '%s'", userName)
rows, err := db.QueryContext(ctx, query) // If userName is "'; DROP TABLE HumanResources.Department;--" ...
动态表或列名称
你不能用参数化查询来识别表名、列名或其他标识符。 如果您的应用需要动态标识符,请根据允许列表验证:
// Validate against known-safe values.
var validColumns = map[string]bool{
"FirstName": true, "CountryRegionName": true, "TerritoryName": true,
}
func queryByColumn(ctx context.Context, db *sql.DB, column, value string) (*sql.Rows, error) {
if !validColumns[column] {
return nil, fmt.Errorf("invalid column name: %q", column)
}
// Safe to use column directly because it was validated against the allowlist.
query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE [%s] = @p1", column)
return db.QueryContext(ctx, query, sql.Named("p1", value))
}
注意
即使进行了方括号转义,也绝不要将用户提供的输入直接用于 SQL 标识符中。 始终根据允许值列表进行验证。
存储过程与SQL注入
存储过程并不会自动阻止SQL注入。 如果过程内部使用 EXECUTE 或 sp_executesql 串接字符串,仍然可能存在漏洞。 使用参数化调用:
// Parameters are passed safely to the procedure.
_, err := db.ExecContext(ctx, "dbo.SearchEmployees",
sql.Named("searchTerm", userInput))
管理连接字符串机密
连接字符串可以包含密码、客户端秘密和证书路径。 绝不要把它们存储在源代码里。
使用环境变量
从环境变量读取完整的连接字符串:
connString := os.Getenv("MSSQL_CONNECTION_STRING")
if connString == "" {
log.Fatal("MSSQL_CONNECTION_STRING environment variable is required")
}
db, err := sql.Open("sqlserver", connString)
从各个机密值生成连接字符串
根据每个组成部分各自独立的环境变量组装连接字符串:
import (
"net/url"
"os"
)
query := url.Values{}
query.Add("database", os.Getenv("DB_NAME"))
query.Add("encrypt", "true")
password := os.Getenv("DB_PASSWORD")
u := &url.URL{
Scheme: "sqlserver",
User: url.UserPassword(os.Getenv("DB_USER"), password),
Host: os.Getenv("DB_HOST"),
RawQuery: query.Encode(),
}
db, err := sql.Open("sqlserver", u.String())
完全取消密码
最安全的连接字符串完全不包含密码。 将 Microsoft Entra ID 身份验证与托管标识配合使用:
import _ "github.com/microsoft/go-mssqldb/azuread"
// No password, no secret. The managed identity handles authentication.
db, err := sql.Open("azuresql",
"sqlserver://<server>.database.windows.net?database=AdventureWorks2025&fedauth=ActiveDirectoryDefault&encrypt=true&TrustServerCertificate=false")
秘密存储选项
| 环境 | 建议的存储 | 备注 |
|---|---|---|
| 地方发展 | 环境变量或用户秘密文件 | 不要将 .env 文件签入到源代码控制中。 |
| Azure 应用服务 / Container Apps | 应用设置或 密钥保管库 引用 | 密钥保管库引用在运行时会注入机密作为环境变量。 |
| Kubernetes | Kubernetes 机密或含 CSI 驱动程序的 Azure 密钥保管库 | 秘密被挂载为文件或环境变量。 |
| CI/CD 管道 | 管道机密/GitHub Actions 机密 | Use ActiveDirectoryServicePrincipal or ActiveDirectoryAzurePipelines for Azure SQL. |
Important
添加 .env、、 *.pfx和 *.pem 到你的 .gitignore 档案中。 意外向仓库提交密钥是最常见的凭证泄露来源之一。
配置加密
所有远程连接都使用加密
为所有远程连接设置 encrypt=true。 仅由服务器要求的加密易受中间人攻击。 客户端必须强制加密。 Azure SQL 默认启用加密:
sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true
使用TDS 8.0以获得最强的加密
TDS 8.0(严格模式)在交换任何TDS协议数据前执行TLS握手,防止协议降级攻击:
sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=strict
TDS 8.0 需要 SQL Server 2022 或 Azure SQL。
不要在生产环境中使用 TrustServerCertificate
将 TrustServerCertificate=true 设为禁用证书验证,这会使系统遭受攻击者发起的中间人攻击:
// DEVELOPMENT ONLY. Never deploy to production.
connString := "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&TrustServerCertificate=true"
对于生产环境,配置合适的证书验证。 参见 加密与证书。
设置最低TLS版本
强制执行TLS 1.2或更高版本,以防止协议降级攻击。
sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true&tlsmin=1.2
应用最低权限原则
使用数据库范围内用户
创建包含的数据库用户,将范围限定在单个数据库上,而不是可访问多个数据库的服务器级登录。
-- Create a contained database user (no server-level login needed).
CREATE USER [app_user] WITH PASSWORD = '<password>';
-- Grant only the permissions the application needs.
ALTER ROLE db_datareader ADD MEMBER [app_user];
ALTER ROLE db_datawriter ADD MEMBER [app_user];
GRANT EXECUTE ON SCHEMA::dbo TO [app_user];
独立的读写连接
如果你的应用有独立的读写路径,创建带有适当权限的独立数据库用户。
// Read-only queries use a restricted user.
readDB, _ := sql.Open("sqlserver",
"sqlserver://app_reader:password@<server>?database=AdventureWorks2025&ApplicationIntent=ReadOnly&encrypt=true")
// Write operations use a user with write permissions.
writeDB, _ := sql.Open("sqlserver",
"sqlserver://app_writer:password@<server>?database=AdventureWorks2025&encrypt=true")
避免在应用中使用SA或DBO
sa登录和dbo用户均拥有不受限制的访问权限。 如果连接 sa 的应用存在SQL注入漏洞,攻击者将完全控制数据库服务器。
保护“Always Encrypted”密钥
使用始终加密时,列主密钥(CMK)保护列加密密钥(CEK)。 妥善保护CMK:
| 密钥提供者 | 最佳做法 |
|---|---|
| 本地证书(PFX) | 将 PFX 文件存储在应用目录之外。 设置限制性文件权限。 用环境变量来设置密码。 |
| Windows 证书存储 | 将 CurrentUser\My 用于服务账号。 通过使用 certutil. 来设置证书权限。 |
| Azure 密钥保管库 | 使用托管身份访问。 按最小权限原则设置密钥保管库访问策略。 启用软删除和清除保护。 |
关于密钥提供者配置,请参见 “始终加密”。
安全记录
不要记录连接字符串或凭证
连接字符串可能包含最终会进入日志文件的密码:
// WRONG: Logs the password.
log.Printf("Connecting to %s", connString)
// CORRECT: Log only the server and database.
log.Printf("Connecting to server=%s database=%s", serverName, dbName)
如果必须记录连接详细信息来用于诊断,请先对连接字符串进行脱敏处理:
func redactSQLServerURL(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return "<invalid connection string>"
}
if u.User != nil {
username := u.User.Username()
if username != "" {
u.User = url.UserPassword(username, "REDACTED")
}
}
q := u.Query()
for _, key := range []string{"password", "clientassertion", "systemtoken"} {
if q.Has(key) {
q.Set(key, "REDACTED")
}
}
u.RawQuery = q.Encode()
return u.String()
}
仅在需要用于短暂诊断时才记录已进行脱敏处理的值。 建议分别记录服务器名称、数据库名称和认证模式。
不要记录敏感的查询参数
避免使用包含个人或敏感数据的日志参数值:
// WRONG: Logs sensitive parameter values.
log.Printf("Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %s", nationalIDNumber)
// CORRECT: Log the query structure without parameter values.
log.Printf("Querying HumanResources.Employee by NationalIDNumber")
在生产环境中谨慎使用日志标志
驱动程序参数 log 可以输出SQL语句和参数值:
| Flag | 风险 | 生产安全? |
|---|---|---|
1 (错误) |
低 | 是的 |
2 (消息) |
低 | 是的 |
4 (行) |
中介,暴露数据 | 否 |
8 (SQL) |
媒介,公开查询 | 有條件的 |
16 (参数) |
高,暴露参数值 | 否 |
32 (交易) |
低 | 是的 |
64 (调试) |
高,会暴露协议细节 | 否 |
生产时,使用 log=1 (仅错误)或 log=3 (错误+消息)。
安全清单
| Area | Recommendation |
|---|---|
| SQL 注入 | 对所有用户输入使用参数化查询。 根据允许列表验证动态标识符。 |
| Credentials | 尽量使用Microsoft Entra管理身份。 绝不要在源代码里硬编码密码。 |
| Encryption | 将所有生产连接设置为 encrypt=true 或 encrypt=strict。 设置 tlsmin=1.2。 |
| 证书 | 请勿在生产环境中使用 TrustServerCertificate=true 。 验证服务器证书。 |
| 最小特权 | 创建仅具备必要权限的应用程序专用数据库用户。 |
| Secrets | 连接字符串存储在环境变量、密钥保管库 或平台秘密存储中。 |
| Logging | 不要记录连接字符串、密码或敏感参数值。 |
| 源代码管理 | 将 .env、*.pfx 和 *.pem 添加到 .gitignore 中。 |