go-mssqldb 的安全最佳实践

本文介绍了通过驱动程序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注入。 如果过程内部使用 EXECUTEsp_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=trueencrypt=strict。 设置 tlsmin=1.2
证书 请勿在生产环境中使用 TrustServerCertificate=true 。 验证服务器证书。
最小特权 创建仅具备必要权限的应用程序专用数据库用户。
Secrets 连接字符串存储在环境变量、密钥保管库 或平台秘密存储中。
Logging 不要记录连接字符串、密码或敏感参数值。
源代码管理 .env*.pfx*.pem 添加到 .gitignore 中。