SQLインジェクション

SQLインジェクションは、 攻撃者が動的なSQLクエリを組み立てる責任があるアプリケーションコードの欠陥を突く手法です。 攻撃者は、アプリケーションの権限が必要な部分にアクセスでき、 データベースからすべての情報を引き出し、既存のデータを改ざんしたり、 危険なシステムレベルのコマンドをデータベースのホスト上で実行できてしまいます。 こうした脆弱性は、開発者が任意の入力をSQL文に結合したり、挿入したりすることで発生します。

例1 表示するデータを分割し ... そしてスーパーユーザーを作成します。(PostgreSQLの例)

以下の例では、ユーザーからの入力が直接SQLクエリに挿入されているため、 攻撃者がデータベースの superuser アカウントを取得できてしまいます。

$offset = $_GET['offset']; // 入力チェックが行われていません!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
通常のユーザーは、$offsetURL に埋め込まれている '次へ'または'前へ'リンクをクリックします。スクリプトは、受け取った $offset が数字であることを期待します。しかしながら、 以下のような値を URL に追加して攻撃を試みるとどうなるでしょうか?
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select 'crack', usesysid, 't','t','crack'
    from pg_shadow where usename='postgres';
--
このようなことが行われると、スクリプトは攻撃者にスーパーユーザー権限での アクセスを提供してしまいます。0;が正しいオフセットを 指していると同時に、クエリをそこで終端させていることに気をつけてください。

注意:

SQLパーサにクエリの残りの部分を無視させるために開発者によく使わ れる技法として、SQLのコメント記号である--があ ります。

パスワードを取得する恐るべき手段に、サイトの検索結果のページを欺く というものがあります。攻撃者が必要な作業は、投稿された変数 の中でSQL命令で使用される際に正しく扱われていないものがあるかどう かを確かめることだけです。これらのフィルタは、通常、 SELECT 文の WHERE, ORDER BY, LIMIT 及び OFFSET 句をカスタマイズするた めに前に置かれる形で設定されます。使用するデータベースが UNION構造をサポートしている場合、 攻撃者は元のクエリに任意のテーブルからパスワードのリストを取得する クエリを追加しようとするかもしれません。 パスワードそのものではなく、セキュアなハッシュ化されたパスワードだけを保存することを強く推奨します。

例2 記事...そして(全てのデータベースサーバーの)いくつかのパスワード のリストを表示する

$query = "SELECT id, name, inserted, size FROM products
WHERE size = '$size'";
$result = odbc_exec($conn, $query);
クエリの静的な部分は、以下のように全てのパスワードを外部にもらす別の SELECT文と組み合わせることができます。
'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--

UPDATEINSERT 文も、 データベースを攻撃するために使用されます。

例3 パスワードのリセットから ... (全てのデータベースサーバーで)より多 くの権限を得るまで

$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
もし悪意のあるユーザーが管理者のパスワードを変更するために 値 ' or uid like'%admin%$uid に代入するか、または、より多くの権限を得 るために、単純に$pwdhehehe', trusted=100, admin='yesと設定すると、 このクエリは以下のように改謬されてしまいます。
<?php

// $uid: ' or uid like '%admin%
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";

// $pwd: hehehe', trusted=100, admin='yes
$query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;"
;

?>

攻撃者がデータベースの構造に関して最低限の知識を持っていないと攻撃は成功しないのが明らかです。 しかし、その手の情報はたいてい、とても簡単に入手できます。 たとえば、コードの一部がオープンソースソフトウェアの一部のため、 公開されている可能性があります。 こうした情報は、クローズドソースの場合でも漏洩する可能性があります - エンコードされたり、難読化されたり、コンパイルされていてもです。 - さらに、自作のコードであっても、エラーメッセージを表示することで漏れてしまう可能性があるのです。 他の方法としては、ありがちなテーブルやカラムの名前を使うことが挙げられます。 たとえば、'id', 'username', 'password' カラムを持つ 'users' テーブルを使うログインフォームが挙げられます。

例4 データベースホストのオペレーティングシステムを攻撃する (MSSQLサーバー)

恐ろしい例として、 いくつかのデータベースホストで、 オペレーティングシステムレベルのコマンドがアクセスできる方法を示します。

$query = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
攻撃者が、値 a%' exec master..xp_cmdshell 'net user test testpass /ADD' --$prodに投稿した場合、 $query は以下のようになります。
$query = "SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
$result = mssql_query($query);
MSSQLサーバーは、新規ユーザーをローカルアカウント用データベースに追 加するコマンドを含むSQL命令をバッチ実行します。 このアプリケーションがsaで実行され、 MSSQLSERVERサービスが充分な権限で実行されていた場合、攻撃者は このマシンにアクセスする権限を有することになります。

注意:

上記の例は、データベースサーバーの種類に依存しています。 しかし、他の製品に対して同様な攻撃ができないことを意味するもので はありません。使用しているデータベースが他の手段で攻撃できる可能性もあります。

SQL インジェクションで発生する面白い問題の例
この画像は » xkcd から提供いただいたものです。

回避策

SQLインジェクションを回避するおすすめの方法は、 すべてのデータをプリペアドステートメント経由でバインドすることです。 パラメータ化されたクエリは、 SQLインジェクションをすべて防ぐのに十分ではありませんが、 SQL文への入力を与える一番簡単かつ安全な方法です。 WHERE, SET, VALUES 句に与えるすべての動的なデータリテラルは、 すべてプレースホルダーで置き換えなければいけません。 すべての実データは実行中にバインドされ、SQLコマンドとは別に送信されます。

パラメータのバインドは、データに対してのみ使えます。 SQLクエリの他の動的な部分については、 許される値の既知の値でフィルタしなければいけません。

例5 PDO のプリペアドステートメントを使い、SQLインジェクションを回避する

<?php
// SQL の動的な部分は、期待する値になるように検証されています
$sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
$productId = $_GET['productId'];
// SQL はプレースホルダを使って準備します。
$stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// 値は LIKE ワイルドカードを使って与えられます
$stmt->execute(["%{$productId}%"]);
?>

プリペアドステートメントは PDOMySQLi、 そして他のデータベースライブラリでも使えます。

SQLインジェクションによる攻撃は、 セキュリティを考慮して書かれていないコードを攻撃する方法です。 特にクライアント側から入力されるあらゆる種類の入力を決して信用しないでください。これは、select ボックス や hidden input フィールド、Cookie の場合も同様です。最初に示した例は、簡単なクエリが破滅をもたらしうることを示しています。

攻撃を防ぐ戦略として、以下に示すいくつかのコーディングプラクティスに従うことが挙げられます:

  • データベースにスーパーユーザーまたはデータベースの所有者として接続しないでください。 非常に制限された権限を有するカスタマイズされたユーザーを常に使用してください。
  • 指定された入力が期待するデータ型であることを確認してください。 PHPは、 多くの種類の入力検証用関数を有しており、 変数関連の関数文字型関数 にある簡単な関数 (例: それぞれ、is_numeric(), ctype_digit()) や、Perl互換の正規表現のサポートまであります。
  • アプリケーションが数値入力を期待している場合、 ctype_digit() を使ってデータを検証するか、 settype() で暗黙の型変換を行うか、 sprintf() で数値表現を使用することを検討してみてください。
  • データベースがバインド変数をサポートしていない場合は、 データベースに渡される数値以外のユーザー入力を データベース固有の文字列エスケープ関数 (e.g. mysql_real_escape_string(), sqlite_escape_string() など) を使ってクォートしてください。 addslashes() のような汎用関数が使える場面は、 とても限定された環境に限られます。 (MySQL をシングルバイト文字セットで使っていて、かつ NO_BACKSLASH_ESCAPES を無効にしている場合など) よって、この関数は使わないほうが良いです。
  • 正しい手段でも、そうでなくても、データベース固有の情報、特にスキーマに関する情報は出力してはいけません。 エラー出力 および エラー処理およびログ関数 も参照ください。

これらのケースにおいて、スクリプトまたはサポートされている場合はデータベース自体でクエリのログをとることが有益です。 明らかにログは破壊的な行為を防止することはできませんが、 攻撃されたアプリケーションを追跡する際には有効です。ログ自体は有益ではありませんが、含まれている情報は有益です。通常、より詳細なログをとる方が良いでしょう。