この記事では、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())
パスワードを完全に排除しましょう
最も安全な接続文字列はパスワードが全くありません。 管理型IDで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 App Service / Container Apps | アプリ設定またはKey Vault参照 | Key Vaultの参照は実行時に環境変数としてシークレットを注入します。 |
| 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 キーを保護する
Always Encryptedを使う場合、カラムマスターキー(CMK)がカラム暗号化キー(CEK)を保護します。 CMKを適切に確保する:
| キープロバイダー | ベスト プラクティス |
|---|---|
| ローカル証明書(PFX) | PFXファイルをアプリケーションディレクトリの外に保存します。 制限的なファイル権限を設定しましょう。 パスワードには環境変数を使いましょう。 |
| Windows 証明書ストア | サービスアカウントには CurrentUser\My を使いましょう。
certutilを使って証明書の権限を設定しましょう。 |
| Azure Key Vault | アクセスにはマネージドIDを使いましょう。 キーボールトアクセスポリシーを最小権限で設定します。 論理的な削除と消去保護を有効にします。 |
鍵提供者の設定については「 常に暗号化」を参照してください。
安全にログを取ってください
接続文字列や認証情報はログにしないようにしましょう
接続文字列には、ログファイルに残るパスワードが含まれていることがあります:
// 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) |
Medium がクエリを公開する | Conditional |
16 (パラメーター) |
Highはパラメータ値を露出します | いいえ |
32 (取引) |
低 | はい |
64 (デバッグ) |
ハイ、プロトコルの詳細を露出させる | いいえ |
本番環境では、 log=1 (エラーのみ)または log=3 (エラー+メッセージ)を使いましょう。
セキュリティ チェックリスト
| Area | レコメンデーション |
|---|---|
| SQL インジェクション | すべてのユーザー入力にはパラメータ付きクエリを使いましょう。 動的識別子を許可リストと照らして検証します。 |
| Credentials | 可能な限りMicrosoft Entraのマネージドアイデンティティを使用してください。 ソースにパスワードをハードコードしてはいけません。 |
| Encryption | すべての生産接続に encrypt=true または encrypt=strict を設定してください。
tlsmin=1.2を設定します。 |
| 証明 書 | 運用環境では TrustServerCertificate=true を使用しないでください。 サーバー証明書の検証。 |
| 最小特権 | 必要な権限のみを持つアプリケーション固有のデータベースユーザーを作成します。 |
| Secrets | 接続文字列は環境変数、Key Vault、プラットフォームの秘密ストアに保存します。 |
| Logging | 接続文字列、パスワード、機密パラメータ値は記録しないでください。 |
| ソース管理 |
.env、*.pfx、*.pemを加えて、.gitignoreに加えましょう。 |