はじめに
SQLインジェクションを根本から断つ最強の仕組み
SQLインジェクション対策の教科書を開くと、
必ず最初に登場するのが Parameterised Query(パラメータ化クエリ)。
しかし、
「値を安全に渡せます」
という一言で片づけられがちで、その本質を深く理解しているエンジニアは意外と少ない。
この記事では、
仕組み → 書き方 → 何を防げるか → 何を防げないか
まで丁寧に解説し、最後に実務で必要な“安全な DB レイヤー設計”まで踏み込む。
1. Parameterised Query とは?
SQL文そのもの(構造)と、実際の入力値(パラメータ)を完全に分離して実行する仕組み。
- SQL文 → 完全に固定のテンプレート
- 値(ユーザー入力) → ドライバが安全にエスケープしてバインド
この“分離”こそが、SQLインジェクションを防ぐ最大のポイント。
2. なぜ必要なのか?(インジェクションの危険性)
悪い例は有名だけど復習。
username = input_user
sql = "SELECT * FROM users WHERE username = '" + username + "';"
cursor.execute(sql)
入力がこれだと:
' OR 1=1 --
SQLはこう壊れる:
SELECT * FROM users WHERE username = '' OR 1=1 --';
→ 全件ダンプ
→ ゲームオーバー
3. パラメータ化クエリの正しい姿
【Python / sqlite3 の例】
cursor.execute(
"SELECT * FROM users WHERE username = ?",
(input_user,)
)
【Python / psycopg2(PostgreSQL)】
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(input_user,)
)
書き方は違っても、本質は完全に同じ。
4. 何がどう安全なのか?(内部動作)
パラメータ化時、SQLドライバは次のように動く:
- SQL構造だけを先に解析・コンパイル(Prepare)
- プレースホルダは「値を入れる穴」として扱われる
- 値は literal(ただの文字列)としてバインドされる
- パラメータは SQL 構造の一部には絶対にならない
つまり ' OR 1=1 -- が来ても、
ただの文字列
として扱われる。
構造に入り込む余地なし。
強すぎる。
5. Parameterised Query が防げるもの・防げないもの
防げる
- 単純な SQL Injection
- WHERE 句内の値インジェクション
- INSERT/UPDATE の値
- LIMIT の値(DBによる)
- ログインフォーム攻撃全般
防げない
-
動的に SQL の“構造”を作るケース
例:
cursor.execute("SELECT " + column + " FROM accounts;")
ここに
column = "*, (SELECT password FROM users)"
が入ったらアウト。
なぜなら:
パラメータ化クエリは“値”だけに効き、カラム名やテーブル名(identifier)には使えない。
SQL仕様として、
SELECT ? FROM table;
は カラム名をパラメータ化できない。
→ これは避けようのない仕様。
6. どうして identifier(カラム名)には使えない?
理由はシンプル。
- パラメータは「literal value」としてしか扱えない
- カラム名やテーブル名は「identifier」
- literal と identifier は SQL の構文階層が別
だから、次は成立しない:
SELECT ? FROM users;
たとえばアプリ側でこう実行すると:
cursor.execute("SELECT ? FROM users;", ("username",))
→ 実行すると 'column_name' という文字列扱いになり、構文エラー。
7. 実務における正しい使い方
原則1:ユーザー入力が SQL 構造に入らないようにする
ダメな例
order_by = input_value
cursor.execute(f"SELECT * FROM users ORDER BY {order_by}")
✔ 良い例(allowlist)
allowed = {"name", "email", "id"}
if order_by not in allowed:
raise ValueError("Invalid order key")
cursor.execute(f"SELECT * FROM users ORDER BY {order_by}")
原則2:WHERE / INSERT / UPDATE の値は必ずパラメータ化
cursor.execute(
"UPDATE users SET email=? WHERE id=?",
(new_email, user_id)
)
原則3:ORM の Query Builder を使うと安全性が爆上がり
SQLAlchemy / Django ORM は
identifier を安全に扱えるように作られている。
8. Parameterised Query + 2nd-order SQL Injection
2nd-order SQLi にも触れておくと、
- Parameterised Query は「その場でのインジェクション」は防げる
- だが悪意ある値を DBに保存 → 別のSQL構文に混ざる
…という攻撃は防げない
例:
- 攻撃者が特殊な値を登録
- 別処理がそれをカラム名として利用
- SQL が破壊される
つまり:
Parameterised Query = 万能ではない
“SQL 構造が動的に変わらない設計”とセットで初めて完全体になる。
まとめ
- Parameterised Query は SQL構造と値を分離する技術
- SQL Injection を最も根本的に防ぐ
- WHERE / INSERT / UPDATE の値は完全に安全
- ただし、カラム名・テーブル名など構造部分は防げない
- 結局重要なのは
→ SQL構造を動的にしない設計
→ allowlist / ORM / QueryBuilder を併用する
パラメータ化クエリはシンプルだけど、
本質を理解すると アプリケーションのセキュリティ設計そのものが変わるくらい強力。