ガイドSC

SQLインジェクション・Prepared Statement・バインド変数

公開: 2026-09-26更新: 2026-09-26
検索APIのSQL生成を例に、値と構文の分離、識別子の許可リスト、ログからの影響調査と再検証を学ぶ。

受注検索APIにbuyerの値を渡すと、別の取引先の注文が表示されました。アプリのコードには『検索文字列をSQLへ付け足す』処理があります。入力に危険な文字があるから問題なのでしょうか。SQLインジェクションの成立条件をSQLの構文とデータの境界から説明し、Prepared Statement・バインド変数、列名の許可リスト、DB権限、影響調査まで一つの検索で追います。

読む順序は、用語→実際の構成と処理→記録の照合→異常の成立条件→変更・復旧→短答演習です。以下の組織、アドレス、時刻、識別子、ログは教材用の架空例です。観測できた事実と、追加調査が必要な推論を分けて読みます。

1. 用語をこの事案の判断に結び付ける

用語

意味とこの事案での判断の限界

SQLインジェクション

アプリが入力値をSQL文字列の構文として解釈させる不備。攻撃者は意図した検索条件や処理を変え得る。入力に引用符があるだけで必ず成立するわけではない。

Prepared Statement

SQLの構造を先に決め、値を別のパラメータとして渡す仕組み。構文とデータの境界を守る。性能上の計画再利用とセキュリティ上の値分離を同じ言葉で呼ぶことがある。

バインド変数

SQL中の`1‘や‘1`や`2`等のプレースホルダへ値を渡す。値にSQLに見える文字があっても、構文ではなく値として扱われる。

動的SQL

テーブル名、列名、並び順、条件数等によりSQLの構造を組み立てること。値だけバインドしても、構造部分へ未検証入力を連結すれば危険が残る。

許可リスト

構文として必要な列名やASC/DESCを、利用者の入力を既知の固定候補へ写して選ぶ方法。文字列に危険文字がないかを見るだけより明確。

最小権限

DB接続アカウントに必要なSELECT等だけを与えること。注入の根本修正にはならないが、成立時にできる操作を限定する。

テナント境界

OrderHubではtenant_id=7の利用者が自テナントの注文だけを見る条件。入力によってWHERE条件が変われば他テナントの情報へ届き得る。

監査ログ

HTTP request ID、認証主体、選択したテナント、DBのクエリテンプレートと実行結果を追う記録。生のパスワードや機密パラメータを安易に記録しない。

2. 構成と判断する位置

OrderHubの利用者U-41はtenant_id=7に属し、GET /orders?buyer=Aliceで注文検索します。API-1(10.20.5.20)はアプリの認証結果からtenant_id=7を取得し、DB-1のorders表を検索します。危険な旧コードはbuyerを引用符で囲んでSQL文字列へ連結していました。

DB-1はtenant_id、buyer、id、totalを持ち、tenant_id=8にも注文があります。注文一覧の閲覧と注文の更新は別のAPI・権限です。この例ではSELECT結果の越境を調べ、単一の文字列入力だけでデータを書き換えられるとは決め付けません。

検索値とSQL構文の境界buyerはAPIに届くが、DBへは構造と値を分けて渡す。利用者U-41API-1DB-11. buyerを送る2. SQL構造と値を送る3. 対象行を返す4. 許可結果を返す
検索値とSQL構文の境界

buyerはAPIに届くが、DBへは構造と値を分けて渡す。

この図のAPI-1からDB-1の線は、Prepared StatementとしてSQL構造と値を別に渡す安全な設計です。旧コードではこの境界でbuyerがSQLの一部になっていました。tenant_idはAPI-1が認証済みセッションから決め、利用者が任意に指定した値は使いません。

3. 正常時の処理と管理

  1. U-41が認証し、API-1がセッションからtenant_id=7を決める。URLのtenant_idをそのまま信頼しない。

  2. API-1が固定のSQL `SELECT id,total FROM orders WHERE tenant_id=1ANDbuyer=1 AND buyer=2`を選ぶ。

  3. パラメータ配列に7とbuyer入力を別々に渡し、DB-1が値として評価する。引用符やSQL予約語を含むbuyerでもWHEREの構造は変わらない。

  4. DB-1は該当テナントとbuyerの行だけ返し、API-1が応答する。0件は検索結果がないことであり、SQLエラーとは違う。

  5. API-1はrequest ID、認証主体、テンプレートID、結果件数を記録し、異常な越境結果を検知する。

安全性は『Prepared StatementというAPI名を使った』だけでは決まりません。SQL文字列を組み立てる前にユーザー入力を連結し、その完成した文字列をprepareしても注入は防げません。固定した構文と値の分離が実際に守られているか確認します。

4. 設定・記録のフィールドを読む

項目

読み方と注意点

request_id / user_id

HTTPとDB実行をつなぐ。ログのIPだけで利用者を特定せず、認証セッションと端末を照合する。

tenant_id

U-41の認証・認可から取得した7。クエリ文字列やbodyに書かれた番号を権限の根拠にしない。

query_template

固定SQLの識別子。値を埋め込んだ全文SQLを秘密情報込みでログへ出さない。

bind_params

型と位置を確認。1=tenantid、1=tenant_id、2=buyerの取り違えは検索漏れや越境の原因。

sort_key / direction

列名とASC/DESCは通常値としてバインドできない。固定候補へのマッピングで構文を選ぶ。

db_role

SELECTのみか、UPDATE/DELETEや他スキーマへ届くか。注入成立時の被害範囲に関わる。

row_count / tenant_set

返却件数と所属テナント。HTTP 200だけでは漏えいの有無を判断できない。

次は架空の検索ログです。攻撃文字列は仕組みを示す最小例であり、実際の影響はDB方言・SQL構造・権限に依存します。

text
12:00:00 api=API-1 req=R-41 user=U-41 tenant=7
  buyer=Alice mode=concat result_rows=2
12:01:00 api=API-1 req=R-42 user=U-41 tenant=7
  buyer=[quote OR true condition] mode=concat
  result_rows=184 tenant_set=7,8
12:03:00 api=API-1 req=R-43 user=U-41 tenant=7
  template=Q-ORDER bind=[7,input] result_rows=0
  tenant_set=none

R-42では旧コードが構文と値を混ぜた結果、tenant_id=8の行も返しています。これはアプリ側の越境結果の証拠ですが、U-41が実際に全184行を閲覧・保存したかはHTTP応答と端末・プロキシログを調べます。R-43は固定テンプレートで同じ入力を値として扱い0件です。`mode=concat`は診断用の架空記録です。

5. 異常が成立する条件と証拠

状態・攻撃

成立条件、証拠、対策の位置

WHEREの改変

旧コードがbuyerを引用符付きで連結すると、入力が引用符を閉じて条件を追加し得る。tenant_id条件まで無効になるかは演算子の優先順位と括弧で決まる。

ORDER BYへの注入

buyerだけバインドしてもsort列をそのまま連結すれば別の入口が残る。許可した列名と方向へ写す。

ORMのraw query

ORMを使っていても生SQL文字列へ入力を連結すれば危険。実際のSQL生成経路とバインド処理を確認する。

動的ストアドプロシージャ

ストアドプロシージャ内部で文字列連結してEXECUTEすれば注入が残る。関数の定義と権限を監査する。

権限の過大付与

DBユーザーにUPDATE/DELETE等があると注入の影響が拡大する。最小権限は補助策だが根本修正の代わりにはならない。

WAF依存

既知パターンの一部は遮断できても変種や内部APIを見逃す。SQL生成の境界を直し、WAFは暫定・補完とする。

入力値に危険な文字があるというだけでは脆弱性は成立しません。成立には、その値が構文としてSQLパーサへ渡される経路が必要です。逆に英数字だけの入力検証を足しても、列名・並び順の動的連結など別の経路が残れば解決しません。

6. 調査で結論を強くする順序

判定段階

必要な証拠と結論の上限

入力経路

HTTPパラメータ、ヘッダ、保存済みデータ、バッチなどSQLへ届く全入力を確認。直接のbuyerだけに限定しない。

構文境界

固定テンプレートとバインドの有無、raw query、動的ORDER BY、ストアドプロシージャを確認。API名だけで安全としない。

実行権限

DBロール、対象表、行レベル制御、関数権限を確認。アプリのtenant条件が外れた時の影響を評価する。

越境結果

DB監査の返却行、API応答、プロキシの送信量、利用者操作を照合。DBが184行返したこととブラウザ表示は別。

修正効果

正規検索と悪意ある形の入力でテンプレートが同じであり、tenant_id=8が返らないことを確認。並び順など別の動的入口も試す。

残留被害

既に返した他テナント情報、ログに残った機密データ、攻撃者の取得範囲を調査し、必要な連絡・通知を判断する。

危険な旧コードの概念例は `sql = prefix + input + suffix` で、prefixがbuyerの引用符を開き、suffixが閉じる形です。入力が引用符を閉じて`OR true`相当の条件を加えると、SQLのWHEREは想定と異なる論理式になります。実際にtenant_id=7の条件が無効になるかは、AND/ORの優先順位、括弧、DBの構文、追加された文字列で決まるため、完成したSQLと実行結果で確認します。

安全な例は `SELECT id,total FROM orders WHERE tenant_id=1ANDbuyer=1 AND buyer=2` と値 `[7,input]` を別に渡します。`input`に引用符やSQLに見える語があっても、DBは文字列の値として照合します。バインドは値に対する対策で、列名やASC/DESCをパラメータに置いて構文として選ぶ用途ではありません。

並び替えを許すなら、外部の`sort=recent`を内部の固定列`created_at`へ、`sort=amount`を`total`へ写し、方向はbooleanや列挙値からASC/DESCを選びます。未知の値は拒否します。SQLへ渡す前に『引用符をエスケープする』だけではDB方言や文字コード差に左右されるため、値のバインドを第一の対策とします。

認証済みU-41でも、自分以外のtenant_id=8の注文を読めるなら権限境界が壊れています。tenant_idをHTTP要求から受け取らずサーバの認証済みセッションから決め、DB側でもアプリ用アカウントの権限を限定します。アプリ層の認可確認とSQLの構文分離は別の防御です。

修正前後の試験では、正常なbuyer=Aliceで自テナントの2件が返ること、特殊文字を含む実在の名前を正しく検索できること、構文に見える入力が0件または該当値だけを返すことを確かめます。単にWAFでR-42だけ遮断しても、別の入力経路が残るためコードとDB権限を確認します。

漏えい調査では、R-42のDB結果184行のうちAPI-1が何行をシリアライズし、HTTPレスポンスとして送ったかを調べます。ページネーション、応答サイズ、エラー、クライアントの受信を照合します。『DBが返した』と『相手が取得した』は別ですが、後者が確認できなくても報告上の未確認リスクは残ります。

7. 変更・障害・例外運用

運用場面

崩れやすい条件と確認

検索機能追加

WHERE、ORDER BY、LIMIT、テーブル選択を一覧化し、値のバインドと識別子の許可リストをコードレビューする。

ORM変更

生成SQLとraw queryの扱いを確認。既存の安全なクエリが文字列連結へ戻らないよう回帰試験を置く。

DB権限変更

API用ロールのSELECT対象と更新権限を見直す。共有の高権限アカウントを全APIで使わない。

緊急遮断

危険な検索エンドポイントだけを一時停止し、注文登録など別業務を維持できるか確認する。恒久修正を後回しにしない。

ログ管理

リクエストIDとテンプレートIDを保存し、機密のbuyerや全SQLを無制限に記録しない。調査用保存は期限と権限を決める。

パッチではSQL連結をバインドへ変え、動的な列名・方向も固定候補へ写します。DBロールを縮小すると正規機能が失敗する場合があるため、必要な業務クエリを一覧化し、検証環境で注文検索・登録・帳票まで回帰確認します。

8. 封じ込めと復旧条件

SQL注入の封じ込めと修正越境結果の調査とコード修正を別に進める。1保全2暫定制限3コード修正4影響確認
SQL注入の封じ込めと修正
  1. 1. 保全:対応 R-42の記録を保存/確認する証跡 APIとDBログ
  2. 2. 暫定制限:対応 危険な検索を止める/確認する証跡 拒否と業務影響
  3. 3. コード修正:対応 構文と値を分離/確認する証跡 生成SQLと試験
  4. 4. 影響確認:対応 越境送信を調べる/確認する証跡 応答と受信範囲

越境結果の調査とコード修正を別に進める。

図のコード修正は将来の注入を止めますが、既にR-42で送った可能性のある注文情報は戻せません。影響調査と関係者への連絡判断を並行して進めます。

  1. R-42を含むAPI、DB、プロキシ、認証のログを保全し、他テナントの行がいつどの主体へ返ったかを調べる。

  2. 危険な検索エンドポイントを一時制限し、代替検索の業務影響を責任者へ伝える。WAFは暫定策として扱う。

  3. buyer等の値をバインドし、sort等の識別子を固定の許可リストへ写し、DBロールを必要最小限にする。

  4. 正常・特殊文字・攻撃形の入力で、生成SQLの構造不変、tenant境界、ページネーション、エラー処理を試験する。

  5. 修正版を配布し、稼働版とログを確認する。越境データの送信範囲と残留リスクを評価し、事業・法務の責任者が再開と通知を判断する。

DBがエラーを返さなくなっただけでは修正完了ではありません。別の検索条件、ソート、帳票、raw queryにも構文連結が残る可能性があります。全SQL生成経路を棚卸しし、同じ境界の回帰試験を置きます。

9. 科目B(午後)の解答手順

設問では入力がどのSQL文のどこへ入るかを示し、値が構文として解釈される条件を書くことが重要です。対策は『入力検証』だけで終えず、値のバインド、識別子の許可リスト、DB権限、修正後の越境試験を役割別に答えます。

  • 引用符を含む入力と注入成立を分ける。

  • prepare前の文字列連結を見落とさない。

  • 値のバインドと列名の許可リストを分ける。

  • DB結果とHTTP送信・受信を分ける。

  • tenant境界とDB権限を確認する。

10. 短答演習

演習1:バインド

条件:buyerへ引用符付きの文字列を渡す。

質問:固定SQL+$2ならWHEREが変わるか。

解答:変わらない。文字列の値として扱われる。

誤答の理由:入力中の文字をSQL構文として評価している。

演習2:prepare前連結

条件:入力で完成したSQL文字列をprepareした。

質問:注入を防げるか。

解答:防げない。構文に入力が既に混ざっている。

誤答の理由:API名だけで値と構文の分離を判断している。

演習3:ORDER BY

条件:sortに利用者入力をそのまま連結。

質問:buyerのバインドだけで安全か。

解答:違う。列名・方向は固定候補へ写す。

誤答の理由:値以外のSQL構文部分を見落としている。

演習4:ORM

条件:ORMのraw queryへ入力を連結。

質問:ORMなら安全か。

解答:安全とは言えない。生成SQLを確認する。

誤答の理由:抽象化の使用を防御の証拠としている。

演習5:184行

条件:R-42のDB結果にtenant 8が含まれる。

質問:184行が利用者へ漏えい確定か。

解答:API応答とクライアント受信を調べる。

誤答の理由:DB返却と外部送信を混同している。

演習6:WAF遮断

条件:既知のR-42文字列をWAFが拒否。

質問:根本修正か。

解答:違う。SQL生成とDB権限を直す。

誤答の理由:入口の一パターン遮断で原因が消えたと考える。

11. 一次資料

OWASP:SQL Injection Prevention Cheat Sheet

PostgreSQL:PREPAREとパラメータ

PostgreSQL:動的SQLと識別子

この記事についてAIに深掘り質問する

ChatGPT、Claude、Perplexityにこの記事を参照させ、要点の確認や疑問点を自由に質問できます。

次におすすめの学習

編集・検証について

編集・検証:IT資格ラボ編集部

IPAが公開する試験要綱・シラバス・過去問題と、各技術の公式資料を優先して内容を確認しています。制度変更や誤りを確認した場合は、記事を見直して更新します。

編集方針・情報源・訂正方針を見る
この記事を共有する