ssecutils
Security / Browser-native guide

SQLインジェクション入門

文字列連結が危険な理由、プレースホルダ、ORM利用時の注意点、Blind SQLi の考え方を解説します。

7Zero tracking reading surface

SQL インジェクションとは

SQL インジェクション(SQLi)は、Web アプリケーションが受け取った文字列をそのまま SQL クエリに連結することで、攻撃者が任意の SQL を実行できてしまう脆弱性です。1998 年に Phrack Magazine で広く知られて以降、四半世紀経った今でも上位の攻撃カテゴリに残り続けています。

OWASP Top 10 - 2021 では A03:2021 Injection として XSS と統合されたカテゴリの中心。情報漏洩・データ改ざん・認証回避・場合によってはサーバー OS への侵入まで、被害の幅広さでは XSS を上回ることもあります。

原因はたった一つ: 文字列連結でクエリを組み立てる

SQLi は教科書的には 「ユーザー入力を、SQL の構文として解釈する場所にそのまま流し込んだ」 という、たった一つのパターンから生まれます。例えば PHP でログイン処理を書く時:

// ❌ 危険なコード
$id = $_POST['id'];
$pw = $_POST['pw'];
$sql = "SELECT * FROM users WHERE id = '" . $id . "' AND pw = '" . $pw . "'";

idadmin' -- という文字列を入れると、組み立て後の SQL はこうなります:

SELECT * FROM users WHERE id = 'admin' --' AND pw = '...'

-- は SQL のコメント開始記号。パスワード判定がコメントとして消滅し、admin としてログインが成立してしまいます。

典型的な攻撃パターン3種

1. 認証回避(Authentication Bypass)

上記の -- 系。' OR '1'='1 もよく見ます:

SELECT * FROM users WHERE id = '' OR '1'='1' AND pw = '...'

OR の優先順位で AND の条件が無視され、1 行目のユーザーでログイン成立。CTF 問題でもおなじみのパターン。

2. UNION ベース攻撃

商品検索のような 「結果を返す」エンドポイントから、別テーブルの内容を取り出す攻撃。

-- 元のクエリ
SELECT name, price FROM products WHERE category = '${q}'

-- 攻撃文字列
${q} = "x' UNION SELECT username, password FROM users --"

-- 組み立て後
SELECT name, price FROM products WHERE category = 'x'
UNION SELECT username, password FROM users --'

商品リストの隣に users テーブルのパスワードハッシュが並んで返ってくる。データベース全体が攻撃対象になる、典型的に被害の大きいパターン。

3. Blind SQLi(盲目型)

エラーメッセージも結果も画面に出ない場合に使う、「条件式の真偽でレスポンスの違いを見る」テクニック:

?id=1 AND SUBSTRING(database(),1,1) = 'a'  → 200 OK
?id=1 AND SUBSTRING(database(),1,1) = 'b'  → 200 OK
...
?id=1 AND SUBSTRING(database(),1,1) = 'm'  → 200 OK
?id=1 AND SUBSTRING(database(),1,1) = 'n'  → 500 Error

1文字ずつ二分探索で確定させていく。手作業だと死ぬほど面倒なので、sqlmap のような自動化ツールが使われます。「画面に何も出ないから安全」は完全な誤解で、Blind SQLi は実質的に UNION 系と同じ情報量を取り出せます。

正しい対策: プリペアドステートメント(パラメータ化クエリ)

SQLi の唯一の本質的な対策がこれです。SQL の構文と値を DB ドライバレベルで完全分離し、値はあくまで「値」としてのみ扱われるように依頼する仕組み。

// ✅ 安全なコード(PHP / PDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND pw = ?");
$stmt->execute([$id, $pw]);

// ✅ 安全なコード(Python / psycopg)
cur.execute("SELECT * FROM users WHERE id = %s AND pw = %s", (id, pw))

// ✅ 安全なコード(Node.js / pg)
await client.query("SELECT * FROM users WHERE id = $1 AND pw = $2", [id, pw])

// ✅ 安全なコード(Go / database/sql)
db.Query("SELECT * FROM users WHERE id = ? AND pw = ?", id, pw)

プレースホルダ(?, $1, %s)の文法は言語ごとに違いますが、原理は同じ:「クエリの形」を先にDBに渡し、「値」は別経路で渡す。値の中に ' OR 1=1 -- が入っていても、それは id 列の値として扱われるので、構文として解釈されません。

ORM を使えば自動で安全?

Prisma / TypeORM / Sequelize / SQLAlchemy / Active Record のような ORM は、普通の使い方をしている限り自動でパラメータ化されます。これが「ORM を入れるとセキュアになる」と言われる根拠。

ただし「生 SQL を書ける機能」を使うと自分でやる必要があります:

// ❌ 危険(Prisma)
prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = '${id}'`)

// ✅ 安全(Prisma)
prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`  // タグ付きテンプレート

// ❌ 危険(SQLAlchemy)
session.execute(f"SELECT * FROM users WHERE id = '{id}'")

// ✅ 安全(SQLAlchemy)
session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": id})

各 ORM の API は「安全な書き方」と「危険な書き方」が紙一重で並んでいることが多いので、ドキュメントの「raw query」「unsafe」と書かれた API を見たらパラメータ化されているか必ず確認してください。

やってはいけない「対策」

1. ブラックリストでキーワードを弾く

SELECT, UNION, --, ' をリクエストから消す方式。必ずバイパスされます

  • SeLeCt のような大小文字混在
  • SE/**/LECT のようにコメント挿入
  • %53ELECT のように URL エンコード
  • ' OR 1=1 -- をエンコードして二重デコード

WAF が補助的に使う場面はあるが、アプリ層の本対策にはなりません

2. シングルクォートをエスケープ

'\' に置換するアプローチ。文字列値については一見有効ですが、数値カラムや LIKE 句、ORDER BY などでは保護されません

-- 数値カラム(クォート不要)
SELECT * FROM products WHERE id = ${id}
-- id="1 OR 1=1" でバイパス成立

-- ORDER BY(プレースホルダで代用不可)
SELECT * FROM products ORDER BY ${col}
-- col="(SELECT password FROM users LIMIT 1)" 系の攻撃可能

ORDER BY の列名や ASC/DESC のようなプレースホルダ化できない部分はホワイトリスト(許容値の固定リスト)と照合する別アプローチが必要です。

3. 「内部APIだから不要」

管理画面・社内ツール・マイクロサービス間の API も SQLi 対策は必須。境界突破された後の被害最小化(多層防御)の観点で、すべてのSQLでパラメータ化を徹底するのが現代の常識。

多層防御として組み合わせる

プリペアドステートメントが本丸ですが、追加で:

  • 最小権限のDBユーザー: アプリ用ユーザーは SELECT/INSERT/UPDATE/DELETE のみ、DROP TABLE は不可
  • エラーメッセージの本番非表示: SQLSTATE やカラム名がエラーで漏れると Blind SQLi の手間が大幅に減る
  • WAF: 既知のシグネチャを弾く時間稼ぎ
  • 監査ログ: 異常なクエリパターン(複数 UNION、長すぎる WHERE 句)の検知
  • カラム暗号化: パスワードは 適切なハッシュ関数 で保存。SQLi で取り出されてもオフライン解析が困難に

おわりに

SQLi の根は「SQL クエリを文字列連結で組み立てる」だけ。プリペアドステートメント / ORM 経由のクエリビルダを徹底すれば原理的に発生しません。本当に難しい部分は「すでに書かれた数千行のレガシーコードに散らばる文字列連結を、すべて洗い出して直す」という運用面であって、技術的にはとてもシンプルな話です。

SQLi は XSS と並ぶ Injection 系の代表で、両方とも OWASP Top 10 の A03 に分類されます。「ユーザー入力を別言語の構文として解釈する場所に出力する時は必ずエスケープ/パラメータ化」という共通原則を意識してみてください。