SQLインジェクション

SQLインジェクション

説明

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障害を区別してください。

参考資料