本文將介紹使用 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 注入。 如果程序內部使用 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")
秘密儲存選項
| 環境 | 推薦儲存方式 | Notes |
|---|---|---|
| 地方發展 | 環境變數或使用者秘密檔案 | 不要將 .env 檔案提交到原始碼控制中。 |
| Azure App 服務 / Container Apps | 應用程式設定或 金鑰保存庫 參考項 | 金鑰保存庫 引用會在執行階段將祕密以環境變數的形式插入。 |
| Kubernetes | 搭配 CSI 驅動程式的 Kubernetes Secrets 或 Azure Key Vault | 秘密會以檔案或環境變數的形式掛載。 |
| CI/CD 管線 | 管線祕密 / GitHub Actions 祕密 | 針對 Azure SQL,請使用 ActiveDirectoryServicePrincipal 或 ActiveDirectoryAzurePipelines。 |
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=true 或 encrypt=strict。 設定 tlsmin=1.2。 |
| 憑證 | 不要在生產環境中使用 TrustServerCertificate=true 。 驗證伺服器憑證。 |
| 最低權限 | 建立僅具備必要權限的應用程式專用資料庫使用者。 |
| Secrets | 連接字串會儲存在環境變數、金鑰保存庫 或平台秘密儲存中。 |
| Logging | 不要記錄連線字串、密碼或敏感參數值。 |
| 源碼管理 | 加上 .env、 *.pfx、 *.pem 到 .gitignore。 |