SQLインジェクション

多くの開発者はSQLクエリがどのように改竄されるかということを余り気にかけておらず、またSQLクエリは信用できるものと考えているようです。 実際にはSQLクエリはアクセス制限を回避することが可能で、従って 通常の認証や権限のチェックを無視することができます。 時には、 OSレベルのコマンドを実行できてしまうこともあります。

ダイレクトSQLコマンドインジェクション(SQLコマンドの直接実行)という手法は、攻撃者がSQLコマンドを生成もしくは既存のコマンドを変更することで隠蔽すべきデータを公開したり、重要なデータを書き換えたり、データベースホストで危険なシステムレベルのコマンドを実行したりするものの事です。 この手法は、ユーザーからの入力をスタティックなパラメータと組み合わせて SQLクエリを生成するアプリケーションにおいて使用されます。以下の例は不幸なことに実際の事例に基づいたものです。

入力のチェックを怠っており、スーパーユーザーもしくはデータベース作成権限を持つユーザー以外のユーザーでデータベースに接続していないために、攻撃者はデータベースにスーパーユーザーを作成することが出来ます。

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

$offset = $argv[0]; // 入力チェックが行われていません!
$query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
通常のユーザーは、$offsetURLに埋め込まれている '次へ'または'前へ'リンクをクリックします。スクリプトは、受け取った $offsetが数字であることを期待します。しかしながら、 攻撃者はurlencode()された以下のような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;
--
('及び--を使用する) このクエリが$queryで使用される変数の1つに代入 された場合、この悪意のあるクエリが実行されることになります。

SQL UPDATE もデータベースを攻撃するために使用されます。これらのク エリも切捨てたり新しいクエリを元のクエリに追加することによる攻撃 を受けます。しかし、攻撃者はSET句を使用する可 能性があります。この場合、クエリを成功させるためにいくつかのスキー マ情報を保有する必要があります。これは、フォームの変数名や総当た り法により調べることができます。パスワードまたはユーザー名を保存す るフィールド用の命名記法はそう多くはありません。

例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
...;"
;

?>

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

例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 から提供いただいたものです。

回避策

攻撃者がデータベースの構造に関して最低限の知識を持っていないと攻撃は成功しないということは明らかですが、 その手の情報はたいてい、簡単に入手できます。 たとえば、オープンソースやその他一般に公開されているソフトウェアパッケージをデフォルトの設定で使っていれば、 データベースの情報は完全に公開されているので誰でも知ることができます。 クローズドソースのコードであってもこの手の情報は漏れることがあります。 たとえ何らかの難読化処理が行われていたとしても。 さらに、自作のコードだとしても、 画面に表示されるエラーメッセージなどから情報が漏れることがあります。 それ以外にも、ありがちなテーブル名やカラム名などは攻撃の対象となります。 たとえば、ログインフォームで使っているテーブル名が 'users' で、その中に 'id'、'username'、'password' といったカラムがある場合などです。

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

  • データベースにスーパーユーザーまたはデータベースの所有者として接続しないでください。 非常に制限された権限を有するカスタマイズされたユーザーを常に使用してください。
  • プリペアドステートメントとバインド変数を使いましょう。 PDOMySQLi で使えるし、その他のライブラリでも提供されている機能です。
  • 指定された入力が期待するデータ型であることを確認してください。 PHPは、多くの種類の入力検証用関数を有しており、 変数関連の関数文字型関数にある簡単な関数 (例: それぞれ、is_numeric(), ctype_digit()) や、 Perl互換の正規表現のサポートま であります。
  • アプリケーションが、数値入力を期待している場合、データを is_numeric()で検証するか、 settype()により暗黙の型変換を行うか、 sprintf()により数値表現を使用することを検討 してみてください。

    例5 ページング用のクエリを構築するためのより安全な方法

    settype($order, 'integer');
    $query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";

    // フォーマット文字列の%dに注意してください。%sを使用しても意味がありません。
    $query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
                     $offset);

  • データベースがバインド変数をサポートしていない場合は、 データベースに渡される数値以外のユーザー入力を データベース固有の文字列エスケープ関数 (mysql_real_escape_string(), sqlite_escape_string() など) でクオートしてください。 addslashes() のような汎用関数が使える場面は非常に限られています (MySQL をシングルバイト文字セットで使っていて、かつ NO_BACKSLASH_ESCAPES を無効にしている場合など)。 なので、addslashes() などの関数を使ってはいけません。
  • データベース固有の情報、特にスキーマに関する情報は出力してはい きません。エラー出力およ びエラー処理およびログ関数 も参照ください。
  • ユーザーがテーブルまたはビューに直接アクセスできないように、 データアクセスを抽象化することを目的としてストアドプロシージャ 及び事前に定義したカーソルを使用することもできますが、このソリューションには、副作用があります。

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