CyberFix Note
脆弱性・CVE解説

SQLインジェクションとは何か。仕組み・攻撃手法・影響・対策を原理から徹底解説

対象の目安: Webアプリ開発者 / 実務レベル

ソウ攻撃・脆弱性リサーチ担当
・ 約13分で読めます

SQLインジェクションは、登場から20年以上たった今もWebアプリケーションの代表的な脆弱性であり続けています。古典的でありながら、攻撃が成立すればデータベース全体の漏えいや改ざん、さらにはサーバーの乗っ取りにまでつながる影響の大きさから、いまも国内外で繰り返し被害が報告されています。OWASP Top 10でも、インジェクションは長年にわたり上位カテゴリに位置づけられてきました。

やっかいなのは、対策そのものは確立されていて難しくないにもかかわらず、いざ開発の現場になると「とりあえず動くSQL」を文字列連結で書いてしまい、脆弱性が作り込まれ続けている点です。この記事では、なぜこの脆弱性が生まれるのかという原理から、攻撃手法の分類、想定される影響、そして根本対策と多層防御までを、順を追って体系的に整理します。

なぜSQLインジェクションは起きるのか

問題の本質は、SQL文という「命令」と、利用者が入力した「データ」を、文字列連結で混ぜてしまう点にあります。アプリケーションが次のようにSQL文を組み立てているとします。

SELECT * FROM users WHERE name = '入力値';

ここで利用者が name' OR '1'='1 という文字列を入力すると、組み上がるSQL文は次のようになります。

SELECT * FROM users WHERE name = '' OR '1'='1';

'1'='1' は常に真になるため、本来1件も返らないはずの条件がすべての行に一致してしまいます。データベースから見れば、これは「正しい構文のSQL文」であり、どこまでが開発者の意図した命令で、どこからが利用者の入力なのか、知るすべがありません。命令とデータの境界が、文字列連結という操作によって壊されてしまったのです。これがSQLインジェクションの核心です。

重要なのは、これが「特定の文字をうっかり通してしまった」という表層的な問題ではなく、「命令とデータを混ぜて文字列として組み立てている」という構造的な問題だという点です。だからこそ、後述するように個別の文字をエスケープして塞ぐ発想ではなく、そもそも命令とデータを分離する発想が必要になります。

攻撃手法の分類

SQLインジェクションと一口に言っても、攻撃者が結果をどう受け取るかによっていくつかの型に分かれます。代表的なものを整理します。

分類概要特徴
インバンド(UNIONベース)UNION SELECT で別テーブルの内容を結果に紛れ込ませる結果が画面に直接表示される場合に強力
インバンド(エラーベース)わざとエラーを起こし、エラーメッセージに情報を含ませる詳細なエラー表示が攻撃者の助けになる
ブラインド(ブーリアン)条件の真偽でページの表示が変わることを利用して1ビットずつ推測する画面に結果が出なくても成立する
ブラインド(時間ベース)SLEEP() 等で応答時間を変え、真偽を時間差で判定する表示の変化すらなくても成立する

ここで強調したいのは、ブラインドSQLインジェクションの存在です。「画面にデータベースの内容を表示していないから安全」という思い込みは誤りです。応答の真偽や時間差だけを手がかりに、攻撃者は1文字ずつデータを復元できます。時間はかかりますが、自動化ツールを使えば現実的な時間で大量のデータが抜き取られます。

注意

攻撃の検証は、必ず自分が管理する環境、または明示的に許可を得た対象に対してのみ行ってください。許可のないシステムへの調査・攻撃は、不正アクセス禁止法をはじめとする法令に抵触します。本記事の手法はすべて、自社システムの防御を確認する目的で理解してください。

想定される影響

SQLインジェクションが成立したときの影響は、単なる情報漏えいにとどまりません。

影響内容
認証回避ログイン条件を常に真にして他人になりすます
情報漏えい会員情報・認証情報・個人情報・カード情報などを抜き取る
改ざん・削除データの書き換えやテーブルの破壊、Webページの改ざん
権限昇格・横展開DBの機能やストアド機能を悪用し、OSコマンド実行やサーバー侵入につなげる
二次被害窃取した認証情報の使い回しによる他サービスへの不正ログイン

特に、データベースに保存された認証情報が漏えいすると、それ自体の被害に加えて、利用者がパスワードを使い回していた場合に他サービスへの被害(いわゆるリスト型攻撃)へと連鎖します。SQLインジェクション一件が、組織の信用とユーザーの安全の両方を一度に損なう可能性があるということです。

実際の攻撃はどう進むか

攻撃者は、いきなり全データを抜くわけではありません。典型的には次のような段階を踏みます。

  1. 1

    脆弱な入力点を探す

    検索フォーム、ログイン、URLパラメータ、Cookie、HTTPヘッダなど、SQLに渡りうるあらゆる入力に対し、シングルクォートや論理演算子を入れて挙動の変化(エラー、表示の差)を観察します。

  2. 2

    脆弱性の種類を特定する

    結果が表示されるのか、真偽でしか分からないのか、時間差でしか分からないのかを切り分け、UNIONベースかブラインドかを判断します。

  3. 3

    データベースの構造を調べる

    テーブル名やカラム名を管理用のメタ情報から取得し、どこに価値あるデータがあるかを把握します。

  4. 4

    データを抜き出す

    特定した構造をもとに、認証情報や個人情報を取り出します。ブラインドの場合は自動化して1文字ずつ復元します。

防御側の視点では、この各段階で痕跡が残ります。次々とエラーを誘発するリクエストや、UNION SLEEP information_schema といった文字列を含むリクエストは、検知の手がかりになります。

根本対策: プレースホルダ(プリペアドステートメント)

最も確実な対策は、SQL文の「構造」と「値」をデータベースドライバのレベルで分離することです。これをプレースホルダ、またはプリペアドステートメントと呼びます。あらかじめ「ここに値が入る」という穴あきのSQL文をデータベースに渡し、値はあとから別経路で渡します。値はどこまでいっても値として扱われ、SQL文の構造を書き換えることはできません。

主要な言語での書き方を見てみましょう。考え方はどの言語でも同じです。

# Python(悪い例: 文字列連結で脆弱)
cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")

# Python(良い例: プレースホルダ)
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
// PHP(PDO のプリペアドステートメント)
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');
$stmt->execute([':name' => $name]);
// Java(PreparedStatement)
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();

いずれも、値の中身がどんな文字列であっても、それは常に「値」として扱われます。' OR '1'='1 を入れても、それは「' OR '1'='1 という名前を探す」という検索になるだけで、SQL文の論理は一切変わりません。これが根本対策である理由です。

OWASPのSQL Injection Prevention Cheat Sheetは、第一の防御として一貫してプリペアドステートメント(パラメータ化クエリ)の利用を挙げています。

値として渡せない部分の扱い

注意したいのは、テーブル名・カラム名・ORDER BY の方向(昇順/降順)など、SQLの「構造」に当たる部分はプレースホルダの値として渡せないという点です。たとえば並び替えのキーをユーザーに選ばせる機能では、受け取った文字列をそのままSQLに埋めるのではなく、あらかじめ許可するカラム名のリストを用意し、その中に含まれる場合だけ使う「許可リスト方式」で対応します。

# 許可リスト方式: 構造部分は受け取った値をそのまま使わない
ALLOWED_SORT = {"created_at", "name", "price"}
sort_key = sort_key if sort_key in ALLOWED_SORT else "created_at"

よくある誤解と、やってはいけない対策

SQLインジェクション対策には、効果が限定的だったり、かえって危険だったりする「やりがちな対処」があります。

  • エスケープだけで済ませる: 入力に含まれるシングルクォートをエスケープする方法は、対象のDBや文字コード、文脈ごとに正しく行う必要があり、抜けや回避が生じやすい方法です。多層防御の一つにはなりますが、根本対策にはなりません。
  • ブラックリストで危険な単語を弾く: SELECTUNION といった単語を禁止する方法は、大文字小文字の混在、コメントの挿入、エンコードなどで容易に回避され、かつ正規の入力まで弾いてしまいます。
  • ストアドプロシージャを使えば安全という思い込み: ストアドプロシージャの内部で動的SQLを文字列連結で組み立てていれば、同じように脆弱です。安全なのはプロシージャだからではなく、パラメータ化されているからです。

つまり、対策の良し悪しは「危険な入力を頑張って取り除いているか」ではなく、「命令とデータが構造的に分離されているか」で判断するのが正しい見方です。

多層防御: 根本対策に重ねる備え

プレースホルダを徹底したうえで、万一の取りこぼしや別経路の侵入に備えて、次の多層防御を重ねます。

多層防御の構成要素

  • DBユーザーの権限を必要最小限にする(アプリ用ユーザーに不要な管理権限を与えない)
  • 詳細なエラーメッセージを利用者に返さない(攻撃者へのヒントを減らす)
  • 入力値の検証(型・長さ・形式)を行い、想定外の入力を早期に弾く
  • WAFを導入し、典型的な攻撃パターンを検知・遮断する
  • アクセスログを監視し、エラー多発や不審なクエリ文字列を検知する

これらはいずれも単独では破られうるため、あくまで根本対策の上に積む「保険」と位置づけます。最小権限は特に重要で、仮に侵入されても、アプリ用ユーザーにテーブル削除やOS連携の権限がなければ被害を限定できます。

ORMを使っている場合の注意

近年の多くのフレームワークはORM(オブジェクト関係マッピング)を備えており、通常の操作では内部でパラメータ化が行われるため、安全側に倒れます。ただし油断は禁物です。

  • 生SQL(raw query)を書く機能を使う箇所は、結局自分でパラメータ化する責任が残ります。
  • 検索条件やソートを文字列として組み立てて渡すインターフェースは、使い方次第で脆弱になります。
  • ORMが提供する「文字列をそのまま埋め込む」系のメソッドは、名前のとおり危険です。

ORMを使っているからと一律に安全とみなさず、生SQLや動的なクエリ生成を行っている箇所を洗い出して個別に確認してください。

検出とテスト

導入後は、脆弱性が残っていないかを確認します。

  1. 1

    ソースコードを調べる

    文字列連結でSQLを組み立てている箇所を grep などで洗い出します。+ や文字列補間でクエリを作っている箇所が重点対象です。

  2. 2

    動的に検査する

    許可された検証環境で、代表的なペイロード(シングルクォート、OR、コメント、時間遅延)に対して挙動が変わらないことを確認します。専用の検査ツールは、必ず自分の管理下または許可された対象に対してのみ使用します。

  3. 3

    回帰を防ぐ

    静的解析(SAST)をCIに組み込み、新たに文字列連結のSQLが追加されたら検知できるようにします。

よくある質問

入力値のエスケープだけでは不十分ですか?
エスケープは有効な多層防御の一つですが、対象のDBや文字コード、文脈ごとに正しく行う必要があり、抜けが生じやすい方法です。根本対策はプレースホルダであり、エスケープはあくまで補助と考えてください。
ORMを使っていれば安全ですか?
多くのORMは内部でパラメータ化を行うため安全側に倒れますが、生SQLや動的なクエリ生成機能を使う場合は同じリスクが生じます。ORM任せにせず、生SQLを書く箇所は個別に確認してください。
テーブル名やカラム名を動的に変えたい場合は?
テーブル名やカラム名はプレースホルダの値として渡せません。あらかじめ許可する名前のリストを用意し、その中からのみ選ばせる許可リスト方式で対応します。
画面にDBの内容を表示していなければ安全ですか?
いいえ。ブラインドSQLインジェクションは、ページの表示の差や応答時間の差だけを手がかりにデータを1文字ずつ復元できます。結果を表示していなくても、命令とデータが分離されていなければ危険です。
WAFを入れればプレースホルダは不要ですか?
不要にはなりません。WAFは既知のパターンを遮断する多層防御ですが、エンコードや巧妙な変形で回避されることがあります。あくまで根本対策の上に重ねる保険です。
古いコードが大量にあり、一度に直せません。どこから手を付けるべき?
外部から到達しやすく、かつ重要なデータを扱う入力点(ログイン、検索、IDパラメータなど)から優先的にプレースホルダ化します。同時に最小権限とエラーメッセージの抑制を入れておくと、対応中の被害を抑えられます。

まとめ

SQLインジェクション対策チェックリスト

  • 値の埋め込みはすべてプレースホルダ経由になっているか
  • テーブル名・カラム名・ソート方向は許可リストで限定しているか
  • 生SQLを書いている箇所をすべて洗い出して確認したか
  • DBユーザーの権限は必要最小限になっているか
  • 詳細なエラーメッセージを利用者に返していないか
  • 代表的なペイロードで挙動が変わらないことを検証したか

仕組みを理解すれば、SQLインジェクションは「命令とデータを分離する」という一点に集約されることが見えてきます。個別の文字を頑張って取り除くのではなく、そもそも混ざらない設計にする。これがすべての出発点です。関連して、より広いWeb脆弱性の全体像は

で、攻撃者視点の安全な学び方は

あわせて読みたい

CTF入門。Webセキュリティを安全に学ぶ最初の一歩を徹底ガイド

であわせてご覧ください。

出典・参考

この記事をシェア

関連する記事