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())

パスワードを完全に排除しましょう

最も安全な接続文字列はパスワードが全くありません。 管理型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には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 キーを保護する

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に加えましょう。