go-mssqldb 的安全最佳做法

本文將介紹使用 SQL Server 驅動程式連接 Go go-mssqldb 應用程式的安全實務。 它涵蓋了 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")

秘密儲存選項

環境 推薦儲存方式 Notes
地方發展 環境變數或使用者秘密檔案 不要將 .env 檔案提交到原始碼控制中。
Azure App 服務 / Container Apps 應用程式設定或 金鑰保存庫 參考項 金鑰保存庫 引用會在執行階段將祕密以環境變數的形式插入。
Kubernetes 搭配 CSI 驅動程式的 Kubernetes Secrets 或 Azure Key Vault 秘密會以檔案或環境變數的形式掛載。
CI/CD 管線 管線祕密 / GitHub Actions 祕密 針對 Azure SQL,請使用 ActiveDirectoryServicePrincipalActiveDirectoryAzurePipelines

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 Key Vault 使用管理身份來存取。 設定金鑰保險庫存取政策為最小權限。 啟用虛刪除與清除保護。

關於金鑰提供者的設定,請參見「始終加密」。

安全地記錄

不要記錄連線字串或憑證

連線字串可能包含最終會出現在日誌檔案中的密碼:

// 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(列) 媒介,揭露資料 No
8 (SQL) Medium,揭露查詢 Conditional
16 (參數) 高,暴露參數值 No
32 (交易) 是的
64 (除錯) 高,會揭露通訊協定細節 No

生產時,請使用 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