はじめに

SQLインジェクションは、1990年代の古いWebアプリケーションだけに存在する脆弱性ではありません。
入力フォームからSQL文を直接送り込む古典的な攻撃に加えて、現在では次のような場所が攻撃面になっています。
REST APIやJSON
CookieやHTTPヘッダー
GraphQL Resolver
ORMのRaw SQL機能
動的な列名や並び順
CSVインポートや夜間バッチ
KafkaやRabbitMQを経由する非同期処理
自然言語からSQLを生成するLLMエージェント
SQLインジェクションはMITREでCWE-89として分類されています。2025年のCWE Top 25では第2位でした。OWASP Top 10:2025でも、SQLインジェクションを含むInjectionがA05に位置付けられています。過去の攻撃ではなく、現在も優先的に対策すべき脆弱性です。
結論・要点
SQLインジェクションの本質は、データとして扱うべき外部入力が、SQLの命令や構文として解釈されることです。
根本原因は次の構造にあります。
安全でない外部入力
│
▼
SQL文字列へ直接連結
│
▼
SQL構造と入力値の境界が崩れる
│
▼
DBMSが入力値を命令として解釈する
SQLインジェクションは、一つの基準だけでは正しく分類できません。
| 分類軸 | 主な種類 |
|---|---|
| 結果を観測する経路 | In-band、Blind、Out-of-band |
| 情報を推測する方法 | エラー、UNION、真偽差、時間差、DNS・HTTP |
| 実行される時期 | First-order、Second-order |
| SQLを変更する方法 | 条件改変、コメント化、UNION、複文、サブクエリ |
| 注入される場所 | WHERE、ORDER BY、SELECT、INSERT、UPDATE、識別子 |
| データアクセス層 | Raw SQL、ORM、HQL、JPQL、Query Builder |
| 入力経路 | URL、JSON、Cookie、ヘッダー、ファイル、メッセージキュー |
| AI関連 | Prompt-to-SQL、Text-to-SQL攻撃、モデル汚染、スキーマ推測 |
これらは排他的ではありません。
一つの脆弱性が、次のような複数の特徴を同時に持つことがあります。
Second-order
+
Time-based Blind
+
ORM内部のRaw SQL
+
非同期バッチ経由
防御の中心はパラメータ化クエリです。
ただし、テーブル名や列名、並び順などは通常のプレースホルダーでは置き換えられません。識別子にはAllowlist、データベースには最小権限、OOB攻撃には外向き通信制御が必要です。OWASPも、Prepared Statementだけでなく、安全なストアドプロシージャ、Allowlist、最小権限を組み合わせる防御を推奨しています。
この記事で分かること
この記事では、次の内容を説明します。
SQLインジェクションが発生する根本原因
1998年から2026年までの歴史的変化
In-band、Blind、OOBの違い
First-orderとSecond-orderの違い
UNION型、エラー型、時間差型などの分類
ORMやDjangoを使っていても発生する理由
GraphQLや非同期処理との関係
Prompt-to-SQLと従来型SQLインジェクションの違い
ソースコードとログの確認方法
Djangoでの安全な実装
WAF、IPS、EDR、DB監査の役割
動作確認と再発防止
対象読者・前提環境
対象読者は次のとおりです。
Webアプリケーション開発者
Django、Laravel、Spring、Hibernateなどの利用者
データベース管理者
インフラ・セキュリティ担当者
SOC、CSIRT、脆弱性診断担当者
LLMやText-to-SQL機能を開発している人
基本情報技術者試験や応用情報技術者試験の学習者
記事内のコード例は主にPythonとDjangoを使用します。
攻撃文字列の実行方法ではなく、仕組み、確認方法、防御方法を中心に説明します。検証は、必ず自分が管理する開発環境または明示的に許可された環境で行ってください。
SQLインジェクションとは
SQLインジェクションは、外部から制御できる入力を使ってSQL文の一部または全部を作成し、SQLの意味を意図せず変更できる脆弱性です。
MITREのCWE-89では、外部入力に影響される値を使ってSQLコマンドを作成し、SQLの特殊要素を正しく無害化できていない状態として定義されています。
簡単なたとえ
SQL文をレストランの注文票に例えます。
本来、利用者が入力できるのは「料理名」だけです。
注文:カレー
しかし、料理名と店員への命令を同じ欄へ自由に書けると、次のような状態になります。
注文:カレー
命令:ほかの客の注文票もすべて見せてください
アプリケーションが「料理名」と「命令」を分離しなければ、データベースは両方を命令として解釈する可能性があります。
正式には、SQLの構文部分とパラメータ値の境界が失われた状態です。
SQLインジェクションの全体像
flowchart TD
A[SQLインジェクション] --> B[結果の取得経路]
A --> C[実行時期]
A --> D[構文の変更方法]
A --> E[注入位置]
A --> F[アクセス技術]
A --> G[現代的な隣接領域]
B --> B1[In-band]
B --> B2[Blind / Inferential]
B --> B3[Out-of-band]
B1 --> B11[Error-based]
B1 --> B12[UNION-based]
B2 --> B21[Boolean-based]
B2 --> B22[Conditional Error]
B2 --> B23[Time-based]
B3 --> B31[DNS]
B3 --> B32[HTTP]
B3 --> B33[SMB・UNC]
C --> C1[First-order]
C --> C2[Second-order]
D --> D1[Tautology]
D --> D2[Comment Truncation]
D --> D3[Stacked Query]
D --> D4[Subquery]
E --> E1[WHERE]
E --> E2[ORDER BY]
E --> E3[INSERT・UPDATE]
E --> E4[列名・テーブル名]
F --> F1[Raw SQL]
F --> F2[ORM・HQL・JPQL]
F --> F3[API・GraphQL]
F --> F4[バッチ・メッセージキュー]
G --> G1[Prompt-to-SQL]
G --> G2[Text-to-SQL攻撃]
G --> G3[モデルバックドア]
SQLインジェクションの歴史
1990年代:動的Webアプリケーションで問題が表面化
1990年代後半、Webフォームから受け取った値をSQL文字列へ連結する実装が広がりました。
1998年のPhrack記事は、WebアプリケーションからRaw SQLを実行する危険性を説明した初期の重要資料です。記事では、SQL Serverの拡張プロシージャにWeb用アカウントからアクセスさせないことや、Raw SQLを避けることにも言及しています。
初期の代表例は、ログイン処理や検索処理の条件を書き換える攻撃でした。
2000年代:UNION型、エラー型、自動化ツールが普及
SQL文の条件を書き換えるだけでなく、別テーブルの検索結果を結合するUNION型や、データベースエラーへ内部情報を含ませるエラー型が広く知られるようになりました。
DBMS製品や列数、テーブル構造を推測し、情報抽出を自動化する診断・攻撃ツールも発展しました。
2000年代後半:Blind SQLインジェクションが高度化
本番環境で詳細エラーを表示しない設定が一般化すると、攻撃者は画面の内容、HTTPステータス、応答時間などの差から情報を推測するようになりました。
MITREのCAPEC-7も、エラーを非表示にするだけではSQLインジェクションを防げず、Blind SQL Injectionが成立し得ると説明しています。
2010年代:ORM、API、Second-orderが重要化
ORMの標準APIは通常、値をパラメータ化してSQLを生成します。
しかし、次の処理ではSQLインジェクションが残ります。
Raw SQL
HQLやJPQLの文字列連結
動的な列名
動的なテーブル名
独自の検索式
ORMの低水準API
保存済みデータを後からSQLへ連結する処理
MITREはORMを経由する攻撃をCAPEC-109、Hibernateに関する弱点をCWE-564として整理しています。
2020年代:クラウド、非同期処理、LLM連携へ拡大
現在の入力経路は、フォームだけではありません。
Webフォーム
REST API
JSON
Cookie
HTTPヘッダー
CSV
ログ
Webhook
Kafka
RabbitMQ
外部SaaS
LLMの生成結果
│
▼
アプリケーション・ワーカー
│
▼
ORM・Query Builder・Raw SQL
│
▼
データベース
OWASPも、パラメータ、HTTPヘッダー、URL、Cookie、JSON、SOAP、XMLなど、複数の入力経路を検査する必要があると説明しています。
1.In-band SQLインジェクション
In-bandとは
攻撃用の入力を送る経路と、結果を受け取る経路が同じ方式です。
攻撃入力 ──HTTP──▶ Webアプリ ──SQL──▶ DB
▲ │
└──────── HTTPレスポンス ◀─────────┘
代表例は、エラー型とUNION型です。
1-1.エラーベースSQLインジェクション

データベースに意図的なエラーを発生させ、エラーメッセージから情報を取得する方式です。
漏えいする可能性がある情報
DBMS製品とバージョン
テーブル名
列名
データ型
実行されたSQL文
ファイルパス
スタックトレース
条件によっては検索結果の一部
成立しやすい条件
詳細なDBエラーを利用者へ返している
開発用のデバッグ設定が本番で有効
型変換やXML処理などのエラーに値が含まれる
アプリケーションがDBエラーを加工せず返している
注意点
エラーメッセージを非表示にすることは必要です。
ただし、SQL文字列の組み立て方を修正しなければ、Boolean型やTime-based型へ移行される可能性があります。
1-2.UNIONベースSQLインジェクション

本来のSELECT結果へ、別のSELECT結果を結合する方式です。
概念的には次のような構造です。
SELECT product_name, description
FROM products
WHERE category = :category;
脆弱な文字列連結があると、別テーブルの結果が検索結果へ混ぜられる可能性があります。
主な成立条件
元のSQLがSELECT文
注入位置でUNIONが構文上使用可能
結合するSELECT文の列数が一致
対応する列のデータ型に互換性がある
結果の一部が画面やAPIレスポンスへ表示される
主な影響
顧客情報の漏えい
パスワードハッシュの漏えい
APIキーやトークンの漏えい
テナントを越えたデータ参照
データベース構造の把握
UNION型は結果を直接表示できるため高速になり得ますが、取得可能な情報はSQLコンテキスト、DBアカウント権限、レスポンスの表示方法によって変わります。
2.Blind SQLインジェクション

Blind SQLインジェクションとは
SQLの実行結果や詳細エラーが画面へ直接表示されない環境で、アプリケーションの挙動差から情報を推測する方式です。
条件を含む入力
│
▼
SQLを実行
│
┌───┴───┐
│ │
条件が真 条件が偽
│ │
表示、時間、ステータスなどの差を観測
Blind型は直接表示型より時間がかかる傾向があります。
しかし、エラーを隠しただけでは防止できません。
2-1.Boolean-based Blind SQLインジェクション

条件が真の場合と偽の場合のレスポンス差を観測します。
観測される差
「検索結果あり」の表示
HTMLの一部分
JSONの項目数
Content-Length
HTTPステータス
リダイレクト先
Cookie
ETag
キャッシュの動作
攻撃者は、一文字ずつ質問するだけでなく、値の大小を比較して探索範囲を半分にする二分探索を利用する場合があります。
2-2.Conditional Error-based Blind SQLインジェクション

条件が真の場合だけエラーを発生させます。
条件が真 → DBエラー → HTTP 500
条件が偽 → 正常処理 → HTTP 200
エラー本文が非表示でも、次の差が残る場合があります。
ステータスコード
レスポンスサイズ
接続切断
ロードバランサのエラーページ
リトライの有無
エラーベースとBlind型の両方の性質を持つ方式です。
2-3.Time-based Blind SQLインジェクション

条件が真の場合だけ、データベース処理を遅延させます。
条件が真 → 指定時間だけ応答を遅らせる
条件が偽 → 通常速度で応答する
代表的な遅延機能はDBMSごとに異なります。
| DBMS | 遅延に悪用される可能性がある機能 |
|---|---|
| Microsoft SQL Server | WAITFOR DELAY |
| PostgreSQL | pg_sleep() |
| MySQL・MariaDB | SLEEP()、高負荷演算 |
| Oracle Database | パッケージやロック、ネットワーク処理など |
| SQLite | 大量演算、再帰、重い検索処理など |
DBMSごとに構文や利用可能な機能が異なるため、攻撃の成立条件も異なります。
クラウド環境での注意
クラウドでは通常時にも次の遅延が発生します。
オートスケール
コールドスタート
DB接続プール待ち
CDNやWAF
ネットワークの揺らぎ
キャッシュミス
そのため、単発の遅延ではなく、複数回の測定結果から統計的に差を判断される場合があります。
3.Out-of-band SQLインジェクション

3-1.DNS-based OOB

データベースサーバーから、攻撃者が管理するドメインへDNS問い合わせを発生させます。
DNS問い合わせの名前へデータの一部を埋め込める場合、Webレスポンスを経由せず情報が送信される可能性があります。
防御
DBサーバーから任意DNSリゾルバへの通信を禁止
指定した社内DNSリゾルバだけを許可
長いサブドメインや高エントロピーなDNS名を監視
DBホスト起点の異常な名前解決を検知
3-2.HTTP-based OOB

DBMSのネットワーク機能や拡張機能を利用し、外部HTTPサーバーへ通信させます。
防御
DBセグメントからインターネットへのHTTP・HTTPSを原則拒否
プロキシ経由に限定
宛先Allowlistを設定
DBプロセス起点の通信を監視
3-3.SMB・UNC型

Windows環境では、UNCパスへのアクセスがDNSやSMB通信を発生させる場合があります。
条件によってはNTLM認証の試行が外部へ送られる可能性もあります。
防御
DBサーバーから外部へのTCP 445を遮断
不要なファイル参照機能を無効化
NTLMの利用範囲を制限
DBサービスアカウントを最小権限化
4.First-orderとSecond-order
4-1.First-order SQLインジェクション

入力された値が、同じリクエストや同じ処理の中ですぐSQLへ組み込まれます。
入力
↓
脆弱なSQLを生成
↓
直ちに実行
検索欄、ログインフォーム、URLパラメータなどで発生する古典的な方式です。
4-2.Second-order SQLインジェクション

入力値が一度データベースやファイルへ保存され、後の処理でSQLへ連結される方式です。
登録API
│
▼
パラメータ化して安全に保存
│
▼
データベース
│
│ 数時間・数日後
▼
レポート・バッチ・管理機能
│
▼
保存値を文字列連結
│
▼
SQLインジェクション成立
保存時にパラメータ化されていても、取り出した後に不安全なSQLへ連結すれば脆弱になります。
PortSwiggerも、利用者入力が一度保存され、後で別のSQLへ不安全に組み込まれる状態をSecond-order SQL Injectionとして説明しています。
発生しやすい場所
CSVインポート
帳票作成
夜間バッチ
BIダッシュボード
保存済み検索条件
データ移行
監査ログ検索
管理者向け集計
ETL処理
KafkaやRabbitMQのConsumer
外部SaaSから同期したデータ
発見しにくい理由
入力した直後には異常が発生しません。
脆弱性スキャナーやWAFが、登録処理と後続のバッチ処理を関連付けられないことがあります。
5.SQL構文の変更方法による分類
5-1.Tautology-based

Tautologyは「常に真になる条件」という意味です。
WHERE句を常に真にすることで、検索条件や認証条件を無効化する方式です。
主な目的
認証回避
非公開データの表示
テナント条件の解除
論理削除条件の解除
対象行の拡大
5-2.Contradiction-based

条件を必ず偽にします。
直接情報を取得する方式というより、真条件とのレスポンス差を比較して、SQLインジェクションの存在を確認するために使われます。
5-3.コメントによる後続条件の無効化

SQLコメントを利用して、元のSQLの後半を無効化する方式です。
影響を受けやすい条件には次があります。
パスワード照合
is_activeis_deletedtenant_id公開範囲
所有者ID
権限制御
コメント構文はDBMSによって異なります。
5-4.Stacked Query・Piggy-backed Query

元のSQL文を終了させ、後ろへ別のSQL文を追加する方式です。
本来のSQL
+
別のSQL文
影響
INSERT
UPDATE
DELETE
DDL
権限変更
ストアドプロシージャ実行
DBMS機能を介した外部通信
複数SQL文を一度に実行できるかどうかは、DBMSだけでなく、データベースドライバや接続オプションにも依存します。
5-5.Subquery-based

条件式やSELECT列の内部へサブクエリを組み込みます。
サブクエリは次の方式と組み合わせられます。
Boolean-based
Error-based
Time-based
INSERT
UPDATE
存在確認
集計値の推測
UNIONが使えないSQLコンテキストでも、サブクエリが利用できる場合があります。
5-6.Stored Procedure Injection

ストアドプロシージャを呼び出しているだけでは、必ずしも安全ではありません。
アプリケーション
│
▼
ストアドプロシージャ
│
▼
内部で動的SQLを文字列連結
│
▼
SQLインジェクション
安全に構築されたストアドプロシージャはパラメータ化クエリと同等の防御になり得ます。
一方、プロシージャ内部で動的SQLを連結すれば、通常のSQLと同じ脆弱性が発生します。MITREとOWASPも、ストアドプロシージャだけではすべてのSQLインジェクションを防げないと説明しています。
5-7.DBMS機能悪用型

SQLインジェクション成立後、DBMS固有機能へ到達する場合があります。
ファイル読み取り
ファイル書き込み
外部ネットワーク通信
リンクサーバー
拡張機能
ジョブ実行
ユーザー定義関数
OSコマンド実行機能
ただし、OSコマンド実行まで進むには、対応するDBMS機能、権限、OS設定が必要です。
MITREは、SQLインジェクションからコマンド実行へ進む攻撃パターンをCAPEC-108として整理しています。
6.注入される位置による分類
6-1.文字列リテラル
WHERE username = '入力値'
文字列境界を壊せる実装があると、SQL構造を変更される可能性があります。
6-2.数値リテラル
WHERE user_id = 入力値
クォートで囲まれていないため、「シングルクォートを削除するだけ」の対策では防げません。
6-3.WHERE句
検索条件、認証条件、テナント条件などに使われます。
6-4.SELECTリスト
BI、動的レポート、カスタム集計などで発生します。
6-5.ORDER BY
並び替え列やASC・DESCを外部入力から作る場合です。
値用プレースホルダーは、通常、列名やSQLキーワードには利用できません。
6-6.GROUP BY・HAVING
集計レポートや分析画面で発生しやすい場所です。
6-7.LIMIT・OFFSET
ページ番号や表示件数を文字列として連結する実装で発生します。
6-8.INSERT
登録処理でSQL構造を変更されるほか、Second-order攻撃用の値を保存される可能性があります。
6-9.UPDATE
WHERE条件が変更されると、他利用者を含む複数行が更新される可能性があります。
6-10.DELETE
削除条件が変更されると、広範囲のデータが削除される可能性があります。
6-11.識別子
次の名前を動的に作る実装です。
テーブル名
列名
スキーマ名
データベース名
関数名
パーティション名
列エイリアス
OWASPは、テーブル名、列名、並び順など、バインド変数を使用できないSQL構造には、コード内の固定値へのマッピングやAllowlistを使用するよう推奨しています。
7.入力経路による分類
SQLインジェクションの入力元は、検索フォームだけではありません。
| 入力経路 | 例 |
|---|---|
| URLクエリ | ?id=...、?sort=... |
| パスパラメータ | /users/{id}/ |
| POSTフォーム | ログイン、検索、更新 |
| JSON | REST APIのfilter、sort、fields |
| Cookie | トラッキングID、店舗ID、表示条件 |
| HTTPヘッダー | User-Agent、Referer、X-Forwarded-For |
| ファイル | CSV、Excel、XML、JSON |
| Webhook | 外部SaaSからのイベント |
| メッセージキュー | Kafka、RabbitMQ、SQS |
| ログ | アクセスログ、監査ログ |
| GraphQL | QueryやMutationの引数 |
| LLM出力 | 自然言語から生成されたSQL |
特にCookieやHTTPヘッダーは、利用者が操作できない値と誤認されやすいため注意が必要です。
8.ORM・HQL・JPQLインジェクション
ORMを使っていてもSQLインジェクションは発生する
ORMは、データベース操作をプログラム上のオブジェクトとして扱う仕組みです。
通常のAPIを正しく使用すれば、値はパラメータ化されます。
しかし、次の使い方では危険が残ります。
Raw SQL
HQLやJPQLの文字列連結
動的な列名
動的なエイリアス
独自Query Builder
ORMの低水準API
ユーザー入力を展開した
**kwargsカスタムSQL関数
テーブル名を動的に変更するマルチテナント設計
Djangoの公式ドキュメントも、QuerySetはパラメータ化によってSQLインジェクションから保護される一方、Raw SQLや一部のクエリ式では、信頼できない値をSQL文字列へ補間してはいけないと警告しています。
2025~2026年のDjango事例
ORMを使用していても、フレームワーク本体の不具合や高度なAPIの使い方によってSQLインジェクションが発生する可能性があります。
Djangoでは2025年から2026年にかけて、列エイリアス、_connector、order_by()、FilteredRelation、PostGISのRaster Lookupなどに関する複数の潜在的SQLインジェクションが修正されました。
2026年7月28日時点で、Django公式サイトが案内する安定版は6.0.7、LTSは5.2.16です。Django 4.2 LTSは2026年4月7日に延長サポートを終了しています。Django 4.2を使用している環境では、単なるパッチ適用ではなく、サポート中の5.2 LTS以降への移行計画が必要です。
9.GraphQLとSQLインジェクション
GraphQL自体はSQLではありません。
SQLインジェクションが発生するかどうかは、ResolverがGraphQL引数をどのようにデータベース処理へ変換するかで決まります。
GraphQLリクエスト
│
▼
Resolver
│
▼
ORM・Query Builder・Raw SQL
│
▼
データベース
危険な設計
sort引数をそのままORDER BYへ使う
fields引数からSELECT列を文字列生成する
filter文字列をRaw SQLへ変換する
利用者指定のテーブル名や集計関数を使う
GraphQL引数をそのままORMの高度な辞書展開へ渡す
正確な理解
GraphQL SQL Injectionという独立した根本原理があるわけではありません。
GraphQLは、SQLへ到達する入力経路の一つです。
バッチリクエストや深いネストは、攻撃回数や負荷、ログの見え方へ影響する可能性がありますが、それだけでSQLインジェクションが成立するわけではありません。
10.WAF・フィルタ回避と難読化
次の項目は独立したSQLインジェクションの種類ではありません。
WAFや単純なブラックリストを回避する補助技術です。
大文字と小文字の変更
コメントによるトークン分割
URLエンコード
二重エンコード
Unicode表現
改行やタブ
16進表現
重複パラメータ
JSONの重複キー
Content-Typeの不一致
DBMS固有の同義関数
WAFとアプリケーションでデコード回数やパラメータ採用ルールが異なると、検査結果に差が生じます。
クライアント
│
▼
CDN・WAFで1回デコード
│
▼
Webサーバーで再デコード
│
▼
アプリケーション
ただし、「少し難読化すればWAFを容易に無効化できる」と一律には言えません。
実際の可否は、WAF製品、ルール、正規化処理、DBMS、アプリケーション実装によって変わります。
11.NoSQL Injectionとの違い
MongoDBなどに対するNoSQL Injectionは、SQLインジェクションとは別の脆弱性です。
共通点は、入力値が検索命令や演算子として解釈されることです。
しかし、対象となる構文は異なります。
| 種類 | 対象 |
|---|---|
| SQL Injection | SQL文 |
| NoSQL Injection | JSON演算子、検索オブジェクト、NoSQLクエリ |
| LDAP Injection | LDAP検索フィルタ |
| XPath Injection | XPath式 |
| Command Injection | OSコマンド |
| Prompt Injection | LLMへの指示とデータの混同 |
NoSQL InjectionをSQLインジェクションの一種と表現するのは正確ではありません。
OWASP WSTGでも、SQL InjectionとNoSQL Injectionは別の検査項目として整理されています。
12.最新動向:Prompt-to-SQLとText-to-SQL

従来型SQLインジェクションとの違い
従来型では、プログラムがSQLのひな型を持っています。
固定SQL
+
外部入力
Text-to-SQLでは、LLMが自然言語からSQL文全体を生成します。
自然言語
│
▼
LLM
│
▼
生成SQL
│
▼
データベース
この違いは重要です。
LLMが危険なSQLを生成しただけでは、必ずしもCWE-89の古典的SQLインジェクションとは限りません。
次の問題が混在します。
Prompt Injection
不適切な認可
過剰なツール権限
安全でないSQL生成
SQL実行前の検証不足
古典的なSQL文字列連結
データベースの過剰権限
英国NCSCも、Prompt InjectionはSQL Injectionと同一ではなく、SQLのような明確なコードとデータの境界をLLM内部で完全に強制できないと説明しています。
12-1.Direct Prompt-to-SQL

利用者が自然言語で、許可されていないテーブルや操作を含むSQLを生成させようとする方式です。
例として、次の要求があります。
全利用者データを取得する
管理テーブルを参照する
更新や削除を実行する
テナント条件を外す
システムカタログを参照する
プロンプトだけでなく、SQL実行ツールの認可設計が重要です。
12-2.Indirect Prompt-to-SQL

LLMが読み込む外部文書やデータベース内の文章に、隠れた命令が含まれる方式です。
外部文書・メール・DB内文章
│
▼
LLMがRAGで取得
│
▼
文章中の命令へ影響される
│
▼
危険なSQL候補を生成
Second-order型のデータフローとIndirect Prompt Injectionが連鎖する構造です。
12-3.Prompt-to-SQL Injection研究

ICSE 2025では、LangChainやLlamaIndexなどを利用するLLM統合Webアプリケーションを対象に、Prompt-to-SQL Injectionのリスクと防御策を調査した研究が発表されています。複数のLLMと実装例を対象に、自然言語入力が危険なSQL生成へつながる可能性を評価しています。
これは、すべてのText-to-SQL製品で実際の侵害が多発していることを意味しません。
現時点では、重要な研究・設計上の脅威として扱うのが適切です。
12-4.Adversarial Text-to-SQL

AAAI 2025では、Text-to-SQLモデルに対し、入力を調整しながら意図した攻撃的SQLを生成させるAIAという研究フレームワークが発表されました。
12-5.スキーマ推測

Text-to-SQLシステムへ質問を繰り返し、背後のテーブル名、列名、データ型を推測する研究もあります。
NAACL 2025の研究では、事前知識なしにText-to-SQLモデルを調査し、データベーススキーマを再構成する方式が検討されました。
これは古典的SQLインジェクションというより、モデル出力を通じたスキーマ情報漏えいです。
12-6.バックドア型Text-to-SQL

学習データやファインチューニングデータへ毒入りデータを混入し、特定の語句や文字列が入力された場合だけ危険なSQLを生成させる研究があります。
ToxicSQL研究では、少量の汚染データを用いてText-to-SQLモデルへバックドア動作を埋め込む可能性が示されています。これは研究結果であり、一般的な実環境で広範に悪用されていることを示すものではありません。
SQLインジェクションの種類比較
以下は一般的な傾向です。
実際の危険度や速度は、DBMS、権限、ネットワーク、アプリケーション実装で変わります。
| 種類 | 観測経路 | 一般的な速度 | 検知難易度 | 主な条件 |
|---|---|---|---|---|
| Error-based | HTTPエラー | 速い | 低~中 | 詳細エラーが返る |
| UNION-based | HTTP本文 | 速い | 低~中 | 列数・型が一致し結果が表示される |
| Boolean-based | 表示差・状態差 | 遅い | 中~高 | 真偽で挙動差が出る |
| Conditional Error | ステータス・エラー差 | 遅い | 中~高 | 条件付きエラーを起こせる |
| Time-based | 応答時間 | 非常に遅い | 高 | 遅延機能が利用可能 |
| DNS OOB | DNSログ | 中 | 高 | DBからDNS通信可能 |
| HTTP OOB | 外部HTTPログ | 中 | 高 | DBからHTTP通信可能 |
| Second-order | 後続処理 | 条件依存 | 非常に高い | 保存値を後で不安全に利用 |
| ORM Injection | ORM生成SQL | 条件依存 | 中~高 | Raw SQLや動的構造がある |
| Identifier Injection | 列名・テーブル名 | 条件依存 | 中~高 | 識別子を外部入力から生成 |
| Prompt-to-SQL | LLM生成SQL | 条件依存 | 高 | LLMにSQL実行権限がある |
確認方法
1.Raw SQLと危険なAPIを検索する
目的
Djangoプロジェクト内のRaw SQL、低水準API、動的SQL候補を探します。
実行場所
manage.pyが存在するDjangoプロジェクトのルートディレクトリです。
コマンド
rg -n --glob '*.py' \
'cursor\.execute|cursor\.executemany|\.raw\(|RawSQL\(|\.extra\(|extra_context|as_sql\(|order_by\(|annotate\(|aggregate\(' \
.
正常例
検索結果がゼロ、または安全なパラメータ化が確認できる状態です。
cursor.execute(
"SELECT id, email FROM users WHERE email = %s",
[email],
)
要確認例
cursor.execute(
f"SELECT id, email FROM users WHERE email = '{email}'"
)
cursor.execute(
"SELECT id FROM users ORDER BY " + sort_column
)
結果の判断方法
次を手作業で確認します。
SQL文字列へf-stringを使用していないか
+で入力値を連結していないか.format()や%でSQLを組み立てていないか列名やテーブル名が外部入力になっていないか
paramsを使用しているかプレースホルダーをクォートで囲んでいないか
保存済みデータを後続処理で連結していないか
Django公式ドキュメントも、Raw SQLではparamsを使用し、プレースホルダー自体をクォートで囲まないよう警告しています。
2.Djangoのバージョンを確認する
目的
サポート切れや、SQLインジェクション修正前のDjangoを使用していないか確認します。
実行場所
Djangoをインストールしている仮想環境またはコンテナ内です。
コマンド
python -m django --version
Docker環境の例です。
docker exec <Djangoコンテナ名> python -m django --version
正常例
5.2.16
または、現在サポート中の系列の最新パッチです。
異常・要対応例
4.2.16
Django 4.2系列は2026年4月7日にサポートを終了しています。
判断方法
5.2 LTSまたは6.0の最新パッチか確認する
4.2以前なら移行計画を作る
メジャー・マイナー番号だけでなくパッチ番号も確認する
本番と開発環境の両方を確認する
3.ログで確認する
Web・WAFログ
次の兆候を確認します。
同じURLへの大量リクエスト
条件部分だけが少しずつ変化する入力
ステータス200と500の反復
一定時間遅れるリクエスト
異常に長いCookie
不自然なエンコード
同じパラメータの重複
JSONの型や構造の不一致
DB監査ログ
次を確認します。
システムカタログへの不審なアクセス
通常使わない関数
大量の部分文字列処理
不自然な条件式の反復
遅延関数
DDL
権限変更
外部接続機能
異常な大量SELECT
DNS・ネットワークログ
次を確認します。
DBサーバー起点の外部DNS問い合わせ
高エントロピーなサブドメイン
DBセグメントからのHTTP・HTTPS
外部へのSMB接続
DBホストから未知の宛先への通信
対処方法
1.値はパラメータ化する
危険な実装
query = f"""
SELECT id, email
FROM users
WHERE email = '{email}'
"""
cursor.execute(query)
安全な実装
query = """
SELECT id, email
FROM users
WHERE email = %s
"""
cursor.execute(query, [email])
SQL構造とパラメータ値を別々に渡します。
Prepared Statementの安全性を「必ずDBサーバーが先にSQLをコンパイルするから」とだけ説明するのは正確ではありません。ドライバによってはクライアント側で処理する場合もあります。
重要なのは、SQL構造と値をデータベースドライバのパラメータ機構で分離することです。OWASPはパラメータ化クエリを第一の防御策として推奨しています。
2.識別子はAllowlistへ変換する
危険な実装
sort = request.GET.get("sort", "created_at")
users = User.objects.raw(
f"SELECT * FROM users ORDER BY {sort}"
)
安全な実装
from django.core.exceptions import SuspiciousOperation
SORT_MAP = {
"newest": "-created_at",
"oldest": "created_at",
"name": "name",
}
requested_sort = request.GET.get("sort", "newest")
sort_field = SORT_MAP.get(requested_sort)
if sort_field is None:
raise SuspiciousOperation("許可されていない並び順です")
users = User.objects.order_by(sort_field)
利用者入力をSQLの列名として直接使用しません。
アプリケーション内部で定義した固定値へ変換します。
3.ORMの標準APIを優先する
user = User.objects.filter(email=email).first()
通常のQuerySetはパラメータ化されます。
ただし、次の機能は重点的にレビューします。
raw()RawSQLextra()独自の
Funcextra_contextカスタムLookup
ユーザー入力から生成した
order_by()ユーザー入力を展開する
**kwargs独自SQLを実行するManager
4.DBアカウントを最小権限にする
参照専用機能には、書き込み権限を与えません。
公開検索機能
└─ SELECT専用ユーザー
一般アプリ機能
└─ 必要テーブルのみSELECT・INSERT・UPDATE
管理バッチ
└─ 対象スキーマのみ
DDL・マイグレーション
└─ 通常アプリとは別アカウント
通常のWebアプリ用アカウントへ、次の権限を安易に与えないようにします。
CREATE
ALTER
DROP
SUPERUSER
拡張機能作成
ファイル操作
OSコマンド実行
他スキーマへの無制限アクセス
5.DBからの外向き通信を遮断する
DBサーバー
├─ DNS:承認済みリゾルバのみ
├─ HTTP:原則拒否
├─ HTTPS:原則拒否
├─ SMB:拒否
├─ SMTP:拒否
└─ 管理・バックアップ先:必要な宛先のみ
OOB SQLインジェクションだけでなく、侵害後の外部通信やデータ持ち出しも抑制できます。
6.詳細エラーを外部へ返さない
利用者には一般的なエラーを返します。
{
"error": "処理に失敗しました"
}
詳細情報は内部ログへ記録します。
request_id
user_id
endpoint
例外クラス
DBエラーコード
スタックトレース
ただし、エラーを非表示にするだけでは根本対策になりません。
7.WAF・IPS・EDRを補助対策として使う
WAF
既知のSQL構文パターン
異常なエンコード
パラメータ重複
不自然なJSON
大量の類似リクエスト
IPS・NDR
Boolean探索の反復
一定時間の遅延を伴う通信
DBサーバー起点のDNS通信
外部SMB接続
通常と異なるDB通信量
EDR
DBプロセスがシェルを起動
DBサービスアカウントによる異常な子プロセス
不審なファイル作成
PowerShellやコマンドシェルの起動
DBホストからの外部通信
WAF、IPS、EDRは被害軽減や早期発見には役立ちます。
しかし、Second-order、内部バッチ、Raw SQL、LLM生成SQLを含め、すべてのSQLインジェクションを単独で防ぐことはできません。
LLM・Text-to-SQLの対策
推奨構成
flowchart LR
A[利用者の自然言語] --> B[LLM]
B --> C[SQL候補]
C --> D[SQL Parser・AST]
D --> E{ポリシー検査}
E -->|拒否| F[安全なエラー]
E -->|許可| G[読み取り専用DB]
G --> H[行数・列の制限]
H --> I[利用者へ結果返却]
必須制御
SELECT以外を拒否
複数ステートメントを拒否
許可テーブルを限定
許可列を限定
システムカタログを拒否
必須のテナント条件を付与
最大行数を制限
Statement Timeoutを設定
読み取り専用トランザクション
読み取り専用レプリカ
Row-Level Security
クエリコスト上限
SQL監査ログ
高リスク操作は人間が承認
LangChainの公式ドキュメントも、SQLエージェントのサンプルはそのまま本番利用する安全な実装ではなく、狭いDB権限とアプリケーション固有の検証を追加するよう明記しています。
動作確認
1.パラメータがデータとして扱われるか
検証環境で、アポストロフィを含む正常な氏名などを登録します。
O'Reilly
正常
SQLエラーにならない
正しい1件のデータとして保存される
SQL構造は変化しない
ログ上では同じクエリ形状になる
異常
SQL構文エラー
検索結果が不自然に増える
別利用者のデータが表示される
500エラーになる
2.Allowlist外の並び順を拒否できるか
sort=unknown
正常
HTTP 400
安全な既定値へフォールバック
DBへ不正な識別子を送らない
異常
文字列がそのままORDER BYへ入る
DBエラーが返る
SQL全文が画面へ表示される
3.Second-orderを確認する
テスト用データを登録する
管理画面を開く
CSV出力を実行する
夜間バッチを動かす
集計レポートを生成する
登録時だけでなく、保存値が使われるすべての後続処理を確認します。
4.外向き通信を確認する
DBサーバーから、不要な外部通信が遮断されていることを確認します。
正常
DNSは指定リゾルバのみ
HTTP・HTTPSは原則拒否
SMBは拒否
許可されていない宛先への接続はファイアウォールで遮断
異常
DBホストから任意のインターネット宛先へ接続できる
外部DNSサーバーを直接指定できる
TCP 445が外部へ到達する
再発防止
コーディング規約
1. SQL文字列と外部入力を連結しない
2. 値は必ずパラメータとして渡す
3. テーブル名・列名は固定マッピングから選ぶ
4. Raw SQLにはセキュリティレビューを必須とする
5. DB内の保存値も信頼済みとみなさない
6. ORMへ任意の辞書をそのまま展開しない
7. LLM生成SQLを直接実行しない
8. アプリ用DBユーザーへDDL権限を与えない
9. DBからの外向き通信を制限する
10. サポート中の最新パッチを適用する
CI・DevSecOps
SASTでSQL文字列連結を検出
Raw SQL使用箇所を一覧化
DASTを検証環境で実行
IASTで実行時の入力経路を追跡
依存関係スキャン
Djangoなどのセキュリティリリース監視
DB監査ログのテスト
Second-order用の結合テスト
LLM SQLポリシーの自動テスト
設計レビュー
次のデータフローを作成します。
入力元
↓
バリデーション
↓
保存先
↓
再利用する処理
↓
SQL生成部分
↓
DB権限
↓
結果の出力先
入力時点だけでなく、保存後にどこで使われるかを確認することが重要です。
注意点・よくある誤解
シングルクォートを削除すれば安全
安全ではありません。
数値コンテキスト、識別子、ORDER BY、エンコード差、DBMS固有構文などがあります。
入力値をエスケープすれば安全
自前エスケープは、文字コード、接続設定、DBMS、ドライバの違いで破綻する可能性があります。
OWASPは、すべての入力をエスケープする方式を強く非推奨としています。
ORMを使えば安全
通常APIは安全性を高めます。
Raw SQL、動的識別子、高度なクエリ式、フレームワーク本体の脆弱性は別途確認が必要です。
エラーを非表示にすれば安全
Blind SQLインジェクションへ移行する可能性があります。
WAFがあるから安全
WAFは補助対策です。
コード修正、DB権限、ネットワーク制御の代わりにはなりません。
管理画面は社内だけなので安全
社内入力、CSV、ログ、連携データも侵害される可能性があります。
管理画面は高権限DBアカウントを利用している場合があり、被害が大きくなることがあります。
Prompt InjectionはSQL Injectionと同じ
別の脆弱性です。
ただし、LLMがSQL実行ツールを持つ場合、Prompt Injectionが危険なSQL生成と実行へ連鎖する可能性があります。
まとめ
SQLインジェクションの根本原因は、1990年代から変わっていません。
データとSQL命令の境界が崩れる
一方、SQLへ到達する経路は大きく変化しました。
1990年代
フォーム・認証画面
2000年代
UNION・Error・Blind・OOB
2010年代
ORM・API・Second-order
2020年代
GraphQL・クラウド・非同期処理
2025年以降
Prompt-to-SQL・Text-to-SQL・SQLエージェント
対策では、次の順序が重要です。
SQL構造を固定する
値をパラメータ化する
識別子をAllowlistへ変換する
ORMのRaw機能を監査する
DBアカウントを最小権限にする
DBからの外向き通信を制限する
詳細エラーを外部へ返さない
WAF、IPS、EDR、DB監査を組み合わせる
保存済みデータも信頼しない
LLM生成SQLを直接実行しない
フレームワークを最新パッチへ更新する
SQLインジェクションは「特殊文字を禁止すれば終わる問題」ではありません。
入力からデータベースまでのデータフロー、後続バッチ、DB権限、ネットワーク、ORM、生成AIを含めた多層防御が必要です。
FAQ
Q1.SQLインジェクションは現在も多いですか?
脆弱性の発生頻度はフレームワークや開発手法によって変わりますが、2025年CWE Top 25ではCWE-89が第2位です。現在も重要度の高い弱点です。
Q2.Django ORMだけを使えば防げますか?
通常のQuerySetはパラメータ化されます。
ただし、Raw SQL、動的列名、独自SQL関数、辞書展開、サポート切れバージョンなどは別途確認が必要です。
Q3.Prepared Statementで100%安全ですか?
値をSQL構造から分離する主対策として非常に強力です。
ただし、テーブル名や列名などの動的識別子、過剰権限、フレームワーク脆弱性、LLMの認可不足までは解決しません。
Q4.入力チェックだけでは防げませんか?
入力チェックは補助対策です。
正当な文章にはアポストロフィなどの特殊文字が含まれるため、危険文字をすべて禁止する設計は現実的ではありません。
Q5.Second-orderはWAFで防げますか?
困難な場合があります。
入力時と発火時が別処理であり、発火が内部バッチの場合はWAFを通らないためです。
Q6.OOB対策で最も重要なものは何ですか?
DBサーバーからの外向き通信制御です。
DNS、HTTP、HTTPS、SMBを必要な宛先だけに限定します。
Q7.LLMへ読み取り専用DBを与えれば十分ですか?
被害は減らせますが、十分ではありません。
機密情報の過剰取得、テナント越境、スキーマ漏えい、高負荷クエリは依然として起こり得ます。
Q8.WAFとIPSは不要ですか?
不要ではありません。
既知攻撃の遮断、反復試行の検知、OOB通信の発見に役立ちます。ただし根本対策ではありません。
コメント(0件)
まだコメントはありません。最初のコメントを投稿してください!