説明
SQLインジェクションは、ユーザー入力を実行するSQL文へ直接連結したり埋め込んだりすると発生します。"SELECT ... WHERE id = " + userInput のように組み立てると、1 OR 1=1 や 1; DROP TABLE users-- などの入力で構造を変更されるおそれがあります。認証の回避やデータの参照・変更・削除が可能かは、DB権限、ドライバー、設定によって異なります。複数文を一度に実行できるかもドライバーと設定に依存します。
想定される影響
- 他のユーザーの個人情報、アカウント情報、内部設定を取得される可能性があります。
- DB権限の範囲でデータを変更・削除され、テーブルやスキーマを削除されるおそれがあります。
OR 1=1などの条件を挿入され、認証や認可を回避される可能性があります。- 負荷の高いクエリや破壊的な操作で、DBやサービスが停止するおそれがあります。
対処方法
db.Query、db.QueryRow、db.Execでは、ドライバーが対応する?や$1を使い、値を別の引数で渡してください。- ORMやクエリビルダーでも値をバインドするAPIを使い、生のSQLへユーザー入力を連結しないでください。
- 数値IDは
strconv.Atoiなどで検証し、変換エラーを処理してください。 - 列名や並べ替え方法など、バインドできないSQL要素はコードで定めた許可リストから選んでください。
- DBアカウントに不要な
DROP、ALTERなどの権限を与えないでください。 - 詳細なDBエラーは保護された内部ログへ記録し、ユーザーには一般的なエラーを返してください。
例
SQLパラメーターの渡し方を比較するハンドラーの抜粋です。? に対応するドライバーを前提としているため、実際のドライバーに合う構文を使ってください。認証・認可と応答の出力処理は別途必要です。
変更前
go
package main
import (
"database/sql"
"net/http"
)
// 変更前: 入力をSQLへ連結するハンドラー
func unsafeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// クエリパラメーターのidを文字列のまま取得
userID := r.URL.Query().Get("id")
// 危険: ユーザー入力を連結してSQLを作成
// 例: /user?id=1 OR 1=1 --
query := "SELECT name, email FROM users WHERE id = " + userID
row := db.QueryRow(query)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
w.Write([]byte(name + " / " + email))
}
}
変更後
go
package main
import (
"database/sql"
"net/http"
"strconv"
)
// 変更後: パラメーター化した文を使うハンドラー
func safeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userIDStr := r.URL.Query().Get("id")
// 1. 型を検証し、数値でない入力を拒否
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
// 2. パラメーター化した文とプレースホルダーを使用
// - `?` や `$1` などのプレースホルダーへ値をバインド
// 文字列の内容によってクエリ構造が変わらないようにする
const query = "SELECT name, email FROM users WHERE id = ?"
row := db.QueryRow(query, userID)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write([]byte(name + " / " + email))
}
}
解説:
- 変更前:
idをSQL文字列へ直接連結するため、条件を変更される可能性があります。OR 1=1で選択行が増えても、このQueryRowは最初の1行だけを読み、残りを破棄します。複数文による攻撃はドライバー設定とDB権限に依存します。 - 変更後:
strconv.Atoiで数値を確認し、固定したSQLにuserIDを別の引数で渡して、SQL構文として解釈されないようにします。実際のエラー処理ではsql.ErrNoRowsとDB障害を区別してください。