ssecutils
Security / Browser-native guide

SQLインジェクション入門

著者 aki公開 7

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 で取り出されてもオフライン解析が困難に

チェックリスト

  • ☐ すべてのクエリでプレースホルダ(パラメータ化)を使い、文字列連結していない
  • ☐ ORM の「生 SQL / unsafe」系 API を使う箇所を把握し、パラメータ化している
  • ☐ プレースホルダ化できない部分(ORDER BY の列名等)はホワイトリスト照合
  • ☐ アプリ用 DB ユーザーは最小権限(DROP 等は不可)
  • ☐ 本番では SQL エラーの詳細をクライアントに返していない
  • ☐ パスワードは適切なハッシュで保存(漏えい時の被害を限定)
  • ☐ ブラックリスト/クォートエスケープを「本対策」として頼っていない

参考(一次情報)

おわりに

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

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

この記事を書いた人
akiサイバーセキュリティ実務者 / エンジニア

サイバーセキュリティ業界で実務に携わるエンジニア。脆弱性・認証・暗号・ネットワークなど、現場で必要になる知識を開発者・運用担当者向けにかみ砕いて解説しています。

運営者・編集方針について

本記事は一般的な情報提供を目的とした解説であり、特定環境での動作・安全性・成果を保証するものではありません。実際の対策の適用は、各自の環境と責任において判断してください。

Related reading

関連記事

Security22
日本国内の主要セキュリティインシデント事例(2020–2025)国内の主要なセキュリティインシデント12件を一覧で俯瞰し、VPN未更新・委託先/グループ経由・弱い認証という繰り返し現れる侵入経路と失敗パターン、日本企業が今すぐ確認すべき対策チェックリストを事例研究として整理します。
Security12
ノートン vs ウイルスバスター 比較2026 - どちらを選ぶべきかNorton 360とウイルスバスタークラウドを料金・機能・サポート・動作の軽さで比較。VPN・バックアップ内蔵のオールインワンか、Pay Guard・24時間日本語電話サポートの国内安心感か。用途別の選び方を解説します。
Security10
ウイルスバスター クラウド レビュー - 機能・料金・特徴を解説トレンドマイクロのウイルスバスタークラウドの機能・料金プランを解説。AI検知・ランサムウェア対策(フォルダシールド)・Pay Guard・フィッシング対策など、国内シェアNo.1セキュリティソフトの全体像をまとめます。
Security10
ノートン 360 レビュー - 機能・料金・特徴を解説【世界No.1】世界販売台数No.1のノートン(Norton 360)の機能・料金プランを解説。リアルタイム保護・VPN・パスワードマネージャー・クラウドバックアップ・ダークウェブモニタリングなど、オールインワンセキュリティの全体像をまとめます。