SQLインジェクション (SQLi) 対策
GoのDBパッケージでのプリペアドステートメントの使用と効果を検証。
SQLインジェクション (SQLi) は、現代のWebアプリケーションにとって最も深刻な脅威の一つです。これを防御するには、アプリケーションコード内部と、ネットワーク境界の両方で対策を施す多層防御が不可欠です
1.アプリケーション内部の最重要対策
プリペアドステートメント
SQLの構造とデータを完全に分離してデータベースに渡す手法。
db.Prepare()でテンプレートを準備し、stmt.Exec()でデータをバインドする。
パラメータ化クエリ
プリペアドステートメント実行時の、データを引数として渡すクエリ実行方式。
stmt.Exec(value1, value2) のように、ユーザー入力を直接引数に渡す。
プレースホルダ
SQLテンプレート内でデータが埋め込まれる場所を示す記号(例:?)。
2. ネットワーク境界の防御:Webアプリケーションファイアウォール (WAF)
WAF
HTTPリクエストの内容(URL、POSTデータなど)を検査し、既知の攻撃パターン(シグネチャ)に一致するものをブロック。
予備の防壁として機能し、アプリケーション側の設定ミスやバグによる脆弱性を突かれるリスクを低減する。SQLi以外にもXSS、CSRFなど広範囲の攻撃を防御。
RDBMS (MySQL/PostgreSQL) の場合:プリペアドステートメントの検証
Goの標準ライブラリ database/sql と、外部のDBドライバを使います。
| データベース | Goパッケージ名 | インポート名 (Go Get) | 接続文字列の例 (DSN) |
|---|---|---|---|
| MySQL | go-sql-driver/mysql | github.com/go-sql-driver/mysql | user=user password=pass dbname=dbname sslmode=disable |
| SQLite3 | mattn/go-sqlite3 | github.com/mattn/go-sqlite3 | ./test.db |
| SQL Server | denisenkom/go-mssqldb | github.com/denisenkom/go-mssqldb | sqlserver://user:pass@host?database=db |
プリペアドステートメントの検証に使う、最もシンプルなテーブル定義します。
mysql> CREATE DATABASE adventar2025
-> ;
Query OK, 1 row affected (0.01 sec)
mysql> use adventar2025
Database changed
mysql> CREATE TABLE users (
-> id INT AUTO_INCREMENT PRIMARY KEY,
-> name VARCHAR(255) NOT NULL,
-> age INT
-> );
Query OK, 0 rows affected (0.07 sec)
mysql>
・MysqlDB接続
package main
import (
"database/sql"
"fmt"
"log"
// connectDB関数で使用するため追加
_ "github.com/go-sql-driver/mysql"
)
// DSNは環境に合わせて変更。本番環境では必ず環境変数を使用すること。
const DSN = "root:YOUR_DUMMY_PASSWORD@tcp(127.0.0.1:3306)/adventar2025"
// DB接続
func connectDB() (*sql.DB, error) {
db, err := sql.Open("mysql", DSN)
if err != nil {
//log.Fatal(err)の代わりに、エラーメッセージをログ出力し、エラーを返す
log.Println("DB Open Error:", err)
return nil, fmt.Errorf("DB接続エラー: %w", err)
}
err = db.Ping()
if err != nil {
db.Close()
log.Println("DB PingError:", err)
return nil, fmt.Errorf("DB接続認証失敗: %w", err)
}
fmt.Println("DB接続成功完了")
return db, nil
}
Go言語(database/sqlパッケージ)とデータベース(RDBMS)
Goの db.Prepare() や stmt.Exec() メソッドが最も重要な役割
RDBMSの役割:処理のキャッシュと再利用
insertUser 関数の解説
1.プリペアドステートメントの準備(db.Prepare)
// プリペアドステートメント
func insertUser(db *sql.DB, name string, age int) error {
//1.Prepare ステートメント作成準備・値を'?'で指定 usersテーブルに挿入するSQL文
const insertSQL = "INSERT INTO users(name,age) VALUES(?,?)"
//// stmt を定義(初期化)し、プリペアドステートメントを準備する
stmt, err := db.Prepare(insertSQL)
if err != nil {
return fmt.Errorf("プリペアドステートメントの準備失敗: %w", err)
}
defer stmt.Close()
・const insertSQL = "...": 挿入する値の部分を?というプレースホルダで指定したSQLテンプレートを定義しています。
・stmt, err := db.Prepare(insertSQL): Goのdatabase/sqlを通じて、このテンプレートをDBサーバーに送信し、実行計画を作成・キャッシュさせます。返されたstmtオブジェクトは、この準備されたSQLを再利用するために使われます。
・defer stmt.Close(): stmtオブジェクトが保持するDBリソースを、関数終了時に確実に解放するための重要な処理です。
2.セキュリティ検証のための入力準備
このセクションは、プリペアドステートメントがなければ攻撃が成功するであろう悪意のあるデータを作成しています。
// 【セキュリティ検証のポイント】
// 名前としてSQLインジェクションを試みる文字列を渡す
// もしプリペアドステートメントでなければ、この '; DROP TABLE...' が実行されてしまう!
maliciousName := name + "'; DROP TABLE test_users; --"
fmt.Printf("挿入を試みます: Name='%s', Age=%d\n", maliciousName, age)
・maliciousName := ...: ユーザー入力(name)に続けて、SQLインジェクションを試みる文字列('; DROP TABLE test_users; --)を意図的に連結しています。
・もしも、プリペアドステートメントでなく、単なる文字列結合でSQL文が作られていた場合、'で前のクエリを閉じ、;で次のクエリとしてDROP TABLE test_usersが実行されてしまう可能性があります。
・この文字列を次のステップでデータとして渡すことで、防御の有効性を検証します。
3.データの実行とSQLi防御の実証(stmt.Exec)
このセクションが、SQLインジェクションを防ぐメカニズムの中核です。
//2.Exec データの流し込み
// stmt.Exec() は引数 (maliciousName, age) を「データ」としてのみ扱います!
// GoはSQLとは別にこのデータをRDBMSに送り、RDBMSはこれをただの文字列としてテーブルに格納します。
// これにより、SQLインジェクションが防御されます。
_, err = stmt.Exec(maliciousName, age)
// ... (エラー処理) ...
// 検証成功のメッセージを出力。悪意のあるコードは実行されていません。
fmt.Println("挿入成功: SQLインジェクションは発生せず、データとして安全に格納されました。")
return nil
func main() {
//db-Injection.go connectDB関数の呼び出し
db, err := (connectDB())
if err != nil {
fmt.Printf("起動失敗:%v\n", err)
return
}
defer db.Close()
fmt.Println("起動成功")
if err := insertUser(db, "安全なユーザー", 30); err != nil {
log.Println("エラー:", err)
}
}
・stmt.Exec(maliciousName, age): Exec()メソッドは、引数で渡された maliciousName と age の値を、先に準備されたSQLテンプレートのプレースホルダ(?)に安全にバインドします。
・セキュリティの働き: GoのDBドライバとRDBMSは、この入力をSQLコマンドとしてではなく、単なる文字列データとしてのみ認識します。文字列データとして安全に処理されるため、悪意のあるDROP TABLEという文字列も、データベースのnameカラムにエスケープされた状態で格納されるだけで、実行されることはありません。
・結果: 攻撃は失敗し、コンソールには「挿入成功: SQLインジェクションは発生せず、データとして安全に格納されました。」というメッセージが出力され、プリペアドステートメントの有効性が実証されます。
go run main.go db-Injection.go
DB接続成功完了
起動成功
挿入を試みます: Name='安全なユーザー'; DROP TABLE test_users; --', Age=30
挿入成功: SQLインジェクションは発生せず、データとして安全に格納されました。
結合テーブルの場合、親の検証で子は問題ないのか?
基本的にはテーブルごと、正確には実行するSQL文ごとにプリペアドステートメントの準備(db.Prepare)を行うのが原則。トランザクションを使用する場合でも、変わりません。
したがって、あなたのアプリケーションで5種類の INSERT、UPDATE、DELETE があれば、少なくとも5種類のプリペアドステートメント(stmtオブジェクト)を定義し、再利用することを検討すべきだということが見えました。
##NoSQL (例: DynamoDB) の場合:プリペアは不要だが、必須制約の管理が変化
NoSQL DBとGo言語での対応表
| 原則 | DynamoDB/Firestore/MongoDB など | Go言語での対応 |
|---|---|---|
| プリペアドステートメント | 不要。SQLを使わず、APIコール(SDK)でデータを操作するため、SQLインジェクションの心配がありません。 | AWS SDK for Go や Firebase Admin SDK for Go を使用します。 |
| 必須制約 (NOT NULL) | DB側で強制できない。スキーマレス(柔軟な構造)であるため、フィールドが欠けていても保存を許容します。 | go-playground/validator が唯一の防壁。validate:"required" タグを使って、Goサーバー側でデータの必須チェックを徹底的に強制します。 |
参考