SQLインジェクションの深淵(境界値の消失) —— なぜ「' OR '1'='1」でパスワードを素通りできるのか?

2026-11-05 / シリーズ「攻防の物理」第2話 / CyberSecurity, SQL, データベース, AST, Web開発

はじめに:記号を数文字入れただけで、なぜ他人の口座が開くのか?

ログイン画面のパスワード欄に、正しいパスワードではなく次のような奇妙な文字列を入力します。

' OR '1'='1

エンターキーを押した瞬間、認証チェックが完全に無視され、システムに登録された最初のユーザー——通常は最高権限を持つ管理者(admin)としてログインが完了してしまいます。

これが、Web黎明期から現代に至るまで無数の企業データを流出させ続けてきた天敵、**SQLインジェクション(SQL Injection)**の最も基本的な姿です。

「プログラマが文字列を安易に連結(+ や format)したから起きたバグだ」とよく説明されます。しかし、それは表面的な事象に過ぎません。 この問題の真の根源は、「人間が読むテキスト(文字列)」という同一の通信路の中で、「コンピュータへの命令(コード)」と「処理対象の数値や名前(データ)」の物理的な境界線が消失したことにあります。

データベース内部で動くパーサー(構文解析器)の力学から、この攻撃が成立する物理的なカラクリを解き明かします。


1. 境界の消失 —— テキストプロトコルが背負った原罪

コンピュータにとって、「命令」と「データ」は本来まったく別種の存在です。

  • 命令(Code):CPUやエンジンが「どう動くか」を規定する論理ルール(例:SELECT, FROM, WHERE, AND)
  • データ(Data):論理ルールによって受動的に処理されるただの材料(例:"yamada", "secret123")

しかし、リレーショナルデータベース(RDBMS)とのやり取りには、人間にとって扱いやすいSQL(Structured Query Language)という平文テキストが採用されました。これが悲劇の始まりです。

⚠️ 1本のテキストパイプ(平文) SELECT * FROM users WHERE pass = '入力値' 字句解析(Lexer) 文字列をトークンへ分解 構文解析(Parser) AST(構文木)を構築 実行エンジン 木の形のまま実行する DBはこの1本の文字列だけを見て、「どこまでが命令で、どこからがデータか」を自力で推測する

Webサーバーからデータベースへ送られるSQL文は、1本の平坦な文字列(バイト列)に過ぎません。 データベースエンジンは、受け取った文字列を左から右へと1文字ずつ読み進めながら、自力で「どこまでがSQLのキーワードで、どこからがユーザーの入力値なのか」を推測しなければならないのです。


2. 構文解析木(AST)の変形物理 —— 演算子の優先順位が書き換わる瞬間

データベースは、受け取ったSQL文をそのまま実行しているわけではありません。 プログラミング言語のコンパイラと同様に、文字列を**構文解析木(AST: Abstract Syntax Tree)**と呼ばれるツリー構造に変換してから実行プランを組み立てます。

典型的な脆弱なPHPのログイン判定コードを見てみましょう。

// ユーザー入力を文字列連結してSQLを組み立てる最悪の例
$sql = "SELECT * FROM users WHERE username = '" . $user . "' AND password = '" . $pass . "'";
$result = $db->query($sql);

正常な入力の場合の木構造

ユーザーが username = "alice"、password = "pass123" と入力した場合:

SELECT * FROM users WHERE username = 'alice' AND password = 'pass123'

パーサーはシングルクォート(')を見つけると、「ここから次のシングルクォートまでは純粋な文字列リテラル(データ)だ」と判断します。構築される構文木は以下のようになります。

WHERE(論理積: AND) 一致比較(=) 一致比較(=) 列: username 値: 'alice' 列: password 値: 'pass123' ANDが真になるには、左右両方の一致が必要

この場合、ルートノードである AND が真になるには、左側の「ユーザー名の一致」と右側の「パスワードの一致」の両方がTrueにならなければ行は抽出されません。至極真っ当な動作です。

攻撃コード ' OR '1'='1 を注入された場合の物理変形

ここで、攻撃者が username に admin を入れ、password に ' OR '1'='1 を流し込むと、組み立てられるSQL文は次のようになります。

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'

この文字列がパーサーに送り込まれた瞬間、パーサーの内部世界で構造の地殻変動が起こります。

  1. password = '' の2つ目のシングルクォートで文字列リテラルが終了したと解釈される。
  2. 次に現れる OR が、単なるデータではなくSQLの論理演算子(トークン)として認識される。
  3. 演算子の優先順位(AND は OR より優先される)に従い、構文木のルート(根本)が書き換わる。
WHERE(論理和: OR)★ルートが支配された 論理積: AND 一致比較: '1' = '1'(恒真: True) username = 'admin' password = '' 左の枝が False でも、右の恒真式のおかげで OR 全体が True になる

できあがった構文木を評価してみましょう。

  • 左側の枝(username = 'admin' AND password = '')はパスワードが不一致なので False になります。
  • しかし、右側の枝は '1' = '1' という無条件で True になる恒真式です。
  • ルートノードは OR です:False OR True の評価結果は True です。

パスワードの正誤判定は完全に無力化され、データベースは「条件に合致したユーザー一覧」を返します。 その結果、一番最初にヒットしたレコード(admin ユーザー)のセッションが発行され、無血開城となってしまうのです。

触って試す:注入で構文木が変わる体験

🌳 注入で構文木が変わる体験

組み立て方式と入力値を変えると、生成されるSQL文と構文木(AST)の形、そしてログイン判定がどう変わるかを見てみましょう。登録済みユーザーは alice(パスワードは pass123)と admin の2人です。

生成されるSQL文

パーサーが構築する構文木(AST)


3. インジェクションの進化 —— 画面に出ないデータを吸い出す技術

「ログインを突破されるだけなら、参照画面を作らなければ大丈夫か?」というと、全くそんなことはありません。 SQLインジェクションの恐ろしいところは、細工された問い合わせを通じて、データベースの内部構造や他テーブルの情報を根こそぎ吸い出せる点にあります。

① UNION型インジェクション(別テーブルの強奪)

検索結果を表示する画面で、UNION 句を注入して機密テーブルを結合します。

-- 本来のクエリ
SELECT id, name, email FROM products WHERE category = 'book'

-- 注入後
SELECT id, name, email FROM products WHERE category = '' 
UNION 
SELECT id, username, password_hash FROM secret_admin_accounts -- '

製品リストが表示されるはずの画面に、管理者アカウントとパスワードハッシュの一覧が平然とレンダリングされます。

② ブラインドSQLインジェクション(盲目の二十の扉)

エラーメッセージも画面表示も一切出ない場合でも、攻撃者はデータを盗み出せます。 「条件が合致している時だけ、データベースを意図的に待たせる(Sleepさせる)」という**時間差攻撃(Time-based Blind)**です。

-- 1文字目が 'a' なら5秒待て、違うなら即座に返れ
' OR IF(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1) = 'a', SLEEP(5), 0) --

サーバーの応答が「5秒遅れて返ってきた」なら、管理者のパスワードの1文字目は a であると確定します。 これをスクリプトで全文字分、ビット単位で繰り返すことで、画面に何も表示されない壁の向こうから、あらゆるデータを文字通り1文字ずつ絞り出していきます。


4. 物理的防御 —— プリペアドステートメントが「境界」を再構築する

この致命的な欠陥に対して、「入力から ' や -- を削除(サニタイズ)すればいい」という対症療法が長年取られてきました。 しかし、文字コードの解釈の違い(Shift_JISなどのマルチバイト文字の隙間を突く攻撃)やエスケープ漏れにより、破られ続けました。

完全な勝利をもたらした唯一の解決策が、**プリペアドステートメント(静的プレースホルダ / Parameterized Queries)**です。

プリペアドステートメントの物理挙動

プリペアドステートメントは、通信そのものを**「構文の決定フェーズ」と「データの流し込みフェーズ」**の2段階に物理的に分離します。

フェーズ1:構文木の確定(PREPARE) Webアプリ → DB:SQLテンプレートを送信 SELECT * FROM users WHERE pass = ? DB:字句・構文解析を実行し、AST(木構造)を確定・コンパイル ★ プレースホルダ(?)は葉ノードに固定される フェーズ2:値の束縛(EXECUTE) Webアプリ → DB:パラメータを送信 ? の値は「' OR '1'='1」である DB:コンパイル済みの木の「葉」に、文字列をただの「値」として代入 ★ 木の構造は絶対に変わらない DB → Webアプリ:結果を返却(そんなパスワードのユーザーは存在しない)
  1. フェーズ1(コンパイル): データベースは ?(プレースホルダ)の入ったテンプレートを受け取り、構文解析を行って**構文木(AST)の骨格を完全に固定(コンパイル)**します。この時点で「何が命令で、どこが比較演算なのか」の論理構造は固まり、二度と変更できません。
  2. フェーズ2(パラメータバインド): 次に、ユーザーからの入力値だけを別のバイナリデータとして流し込みます。

たとえパラメータの中にどれほど巧妙に ' OR '1'='1 や DROP TABLE が含まれていようとも、データベースはそれを**「そういう内容の1つの長い文字列データ」**としてしか扱いません。

【固定された木構造の末端】
password 列の比較対象:
[ 文字列: "' OR '1'='1" という奇妙なパスワードそのもの ]

構文木の枝分かれ構造が破壊されることは物理的にあり得ないため、インジェクションは100%不発に終わります。


5. まとめ:データとコードを同じパイプに流すな

スタックオーバーフローが「メモリ空間におけるデータと機械語コードの混同」だったとすれば、SQLインジェクションは「テキスト通信路におけるデータと構文トークンの混同」です。

形は違えど、どちらも**「コンピュータが受動的なデータと能動的な命令の境界を見失った瞬間に発生する物理現象」**という点で、根底の原理は同一です。

現代のプログラミング言語(ORMやDBライブラリ)がプリペアドステートメントを標準としているのは、プログラマのタイピングを楽にするためではなく、データが命令へと化ける物理的な通路を遮断するためなのです。


📝 3行で頭に入る『試験・実務の要点まとめ』

  1. 破壊の真因:平文テキスト通信では命令(SQL予約語)とデータ(入力値)が同一視されるため、細工された文字で構文木(AST)の親子関係や優先順位が書き換わる。
  2. ブラインド攻撃:画面上に検索結果やエラーが出なくても、SLEEP()等の応答遅延を利用した「20の扉」方式でデータベース内の全情報を外部へ抽出できる。
  3. 完全防御の原理:文字列置換(エスケープ)ではなく「プリペアドステートメント(静的プレースホルダ)」を使い、SQLの構文木をコンパイルした後に値を後から流し込むことで構造改変を物理的に遮断する。

確認クイズ

📝 確認クイズ(3問)