SQLインジェクション
はじめに
この記事の内容は 「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版」 の第4章4節の要点をまとめたものです。
※ 基本的に引用は避けています。
概要
SQL インジェクションとは、外部の入力があるWEB アプリケーションにおいて、SQL の組み立て方に不備があった場合に発生する脆弱性のことである。
攻撃手法と影響
エラーメッセージ経由の情報漏洩
下記サンプルは、検索フォームがあって、本の情報をユーザーからの入力値で著者名で検索する、といったものを想定している。
<?php ... $sql = "SELECT * FROM books WHERE author = '$_GET[\"author\"]' ORDER BY id"; $result = $db->query($sql); // SQL実行 ?> <tr> <th>ID</th> <th>タイトル</th> <th>著者名</th> </tr> // 本情報出力処理 ...
ここに対して以下のURL にリクエストを送ると
脆弱サイト.php?author='+AND+EXTRACTVALUE(0, SELECT+CONCAT('$', id, ':', pwd) +FROM+users+LIMIT+0, 1 ))+%23
Error: SQLSTATE[HY000]: General Error: 1105 Unknown XPATH variable at '$yujiro:yujiro4649'
と表示される。(yujiro:yujiro4649 はユーザー名とパスワード)
ここでのポイントは、
SELECT CONCAT('$', id, ':', pwd) FROM users LIMIT 0, 1
このSELECT 文によって、users テーブルから1行読み取って、id カラム、pwd カラムを抜き出して'$' と :
で連結している部分。
そのうえで、EXTRACTVALUE
という関数でXPath というものでXML 検索しようとしてエラーになっている。
EXTRACTVALUE
のほうはエラー文を表示するために使っているだけなので、詳細は割愛する。
結果、SQL インジェクションが成功し、ユーザー名とパスワードが割れてしまった。
本番環境であればSQL 実行のエラーメッセージの出力はOFFにしておくのがよい。(これは保険的な対策であって、基本対策としては「対策」項を参考)
UNION SELECT を用いた情報漏洩
エラーメッセージの他に、UNION SELECT を使って情報を抜かれてしまうことがある。
先と同じ例で、
脆弱サイト.php?author='+UNION+SELECT+ id, pwd,name+FROM+users
というGETリクエストを送信された場合、
SELECT * FROM books WHERE author = '' UNION SELECT id,pwd,name FROM users ORDER BY id"
となり、HTML にユーザーのid、パスワード、名前が表示されてしまう。
このように、UNION を使うと、SELECT 句を連結できるので、もともと想定されていたSELECT を終了させられ、 攻撃者が組みたてたSELECT 句を実行されてしまうため、好き勝手にSQL を実行されて情報を抜かれてしまう。
SQL インジェクションによる認証回避
SQL インジェクションによって、ログインなどの認証を回避されてしまうことがある。
例えば下記のように入力されたID とパスワードでSQLを組み立てて、結果によってログイン状態をセットする処理があった場合、
<?php ... $sql = "SELECT * FROM users WHERE id = '$_POST[\"id\"]' AND pwd = '$_POST[\"pwd\"]'"; $result = $db->query($sql); // SQL実行 ?> <html> <body> <?php if ($ps->rowCount() > 0) { // SELECT した行が存在する場合ログイン成功 $_SESSION['id'] = $_POST["id"]; echo "ログイン成功"; } else { echo "ログイン失敗"; } ?> </body> </html>
ここに、
pwd=`OR 'a' = 'a'
とPOSTされれば、
SELECT * FROM users WHERE id = '' AND pwd = '' OR 'a' = 'a'
となり、レコードが全ヒットし、結果としてログインが成功してしまう。
SQLインジェクションによるデータ改ざん
<?php $sql = "SELECT * FROM books WHERE author = '$_GET[\"author\"]' ORDER BY id";
前項までで紹介している上記に
?author=';UPDATE+books+SET+title%3d'cracked!''+WHERE+id%3d'1001'--+
と送信されれば、
SELECT * FROM books WHERE author = ''; UPDATE books SET title = 'cracked' WHERE id = '1001' -- 'ORDER BY id
となる。
ちなみに--
以降はコメントになり無効化される。
これでデータを書き換えが成功してしまう。
原因
これら脆弱性が生まれる原因はリテラルを正しく扱えていないことに起因する。
SQL には文字列リテラル、数字リテラル、日時リテラルなどが存在するが、SQL インジェクションにおいては文字列リテラルと数字リテラルを抑えておけばよい。
文字列リテラル
SQL の標準規格において、文字列はシングルクオートで囲う。
文字列リテラルの中にシングルクオートを含めたい場合はシングルクオートを重ねる。(シングルクオートのエスケープ)
例えば「O'Reilly」という文字列をSQLの文字列リテラルにすると、
'O''Reilly'
となる。
なので、入力値にシングルクオートがあった場合はシングルクオートを重ねてエスケープしなければいけない。
その処理が抜けていると、シングルクオートでSQLを終了させられ、後続に悪意のあるSQLを入れ込まれてしまう。
数値リテラル
下記のプログラムの例で
SELECT * FROM employees WHERE age < $age
数値を想定している$age
に
1; DELETE FROM 〜
といった具合でSQL を入れ込まれたらSQL インジェクションが成立する。
数値リテラルを正しく扱えていない = 文字列を差し込まれてしまう形になっているのが原因。
対策
SQLインジェクションの対策としては2通りある。
後者のほうは完全な対応が難しい。
先述したように文字列リテラルのシングルクオートだけをエスケープすればよいだけでなく、数値リテラルのほうにも対応が必要なため、そのあたりをアプリケーション側の処理でカバーするのが大変。
前者のほうで対応するのがオススメ。
プレースホルダによる対策
プレースホルダを使用すると以下のようにプログラム中にSQLを記述するようになる。
<?php $sql = "SELECT * FROM books WHERE author = ? ORDER BY id";
ここの「?」の部分がプレースホルダにあたる。
つまり可変のパラメータの場所に埋め込むものである。
そして、ここの「?」に値を埋め込むことを「バインド」と呼ぶ。
PHP で記述すると以下のようになる。
<?php $sql = "SELECT * FROM books WHERE author = ? ORDER BY id"; $ps = $db->prepare($sql); $ps->bindValue(1, $_GET["author"], PDO::PARAM_STR);
プレースホルダの種類
プレースホルダには
の2つ種類が存在する。
静的プレースホルダ
静的プレースホルダは、値のバインドをデータベースエンジン側で行うタイプのプレースホルダ。
プレースホルダのついたSQL がデータベースに送られ、コンパイルなど実行準備が行われSQL文が確定する。
そのあとでバインドの処理が入る。
SQL 文の確定 → バインド処理 の順番のため、バインド処理時に構文が変わることがなく、SQLインジェクションが入る余地がなく安全。
動的プレースホルダ
動的プレースホルダは、SQLを呼び出すアプリケーションのライブラリの中で値をバインドしSQLを確定させてから、データベースエンジンに送るタイプのプレースホルダ。
ライブラリの処理にバグがあれば、SQL インジェクションが発生する可能性がある。
まとめ
SQL インジェクションは非常に危険な脆弱性で、1つでもすきがあれば、データベースのすべての情報の漏洩・改ざんにつながることは「攻撃手法と影響」のところで学んだ。