なぜPHPアプリにセキュリティホールが多いのか?

第14回減らないSQLインジェクション脆弱性

SQLインジェクション脆弱性を狙った大規模な攻撃が繰り返し行われ、数万から数十万ページが改竄される事件が何度も発生しています。SQLインジェクションは簡単に対策できる脆弱性ですが、未対策のアプリケーションが多く利用されています。外部からの脆弱性の検出も容易であるため、現在でもWebアプリケーション脆弱性の代表的存在です。

SQLインジェクション脆弱性が無くならない理由には以下のようなものが考えられます。

  • 過去のコードやアプリケーションの再利用
  • 基本的なセキュリティ知識不足
  • セキュアコーディングプラクティスの未実施
  • コード監査の不在

SQLインジェクション脆弱性の発見だけを目的にコード監査を行うことはあまりありませんが、SQLインジェクション脆弱性のコード監査は比較的簡単です。MySQLモジュールまたはPostgreSQLモジュールを利用している場合を例に紹介します。

本題に入る前にSQLインジェクションの基礎知識クイズをしてみてください。いくつの質問に自信を持って答えられるでしょうか? すべて自信がある方はこの記事を読まなくても大丈夫な方です。

SQLインジェクションクイズ

SQLインジェクションに対する知識をクイズとしてお聞きます。○か×かで答え、その理由も考えてください。回答はできるだけ厳格に考えてください。特定の環境や設定など条件が限定的でも構いません。答えは次回に掲載します。

  1. SQLインジェクションはエスケープ処理を確実にしていれば大丈夫?
  2. プリペアードクエリを利用していれば大丈夫?
  3. SQLインジェクションはデータベース構造を知らないと攻撃が難しい?
  4. SQLインジェクションはWebアプリケーションファイアーウォールで防御できる?
  5. 文字エンコーディングベースのSQLインジェクションは文字エンコーディングが正しければ行えない?

SQLインジェクションを目的としたコードのチェック

PHPアプリの場合、プリペアードクエリを利用するアプリケーションより、文字列エスケープを用いてSQLインジェクションを防いでいるアプリケーションが多いと思います。ここではPostgreSQLを利用している場合を例にチェック箇所を紹介します。チェック手順の紹介の前に、SQLインジェクション対策のベストプラクティスを紹介します。

  • 文字エンコーディングが正しいか検証し、文字エンコーディング関連の処理を厳格に行う
  • クエリを生成する場合に「すべて」のパラメータを文字列として扱いエスケープ処理してクエリを生成する
  • プリペアードクエリ生成の際に、プリペアード文にパラメータを入れない
  • テーブル名、フィールド名をパラメータで指定する場合、ホワイトリスト方式で確認する

コードチェックはこれらのベストプラクティスが実践されているかチェックすることが目的となります。これらのベストプラクティスを守っていなくてもSQLインジェクションが不可能なコードを記述可能ですが、SQLインジェクションを確実に防ぐためにはベストプラクティスに従うべきです。

SQLインジェクション脆弱性チェックのポイント

チェックポイント:すべての入力がmb_check_encoding関数で文字エンコーディングチェックされ、不正な入力でスクリプトの実行が停止するか?

壊れた文字エンコーディングを利用したSQLインジェクションを防ぐためには文字エンコーディングが正しいか確認することが最も確実です。SQLインジェクション以外の攻撃にも壊れた文字エンコーディングが利用されるので、アプリケーション全体で入力文字エンコーディングのチェックが必要です。

$_POST、$_GET、$_COOKIE、$_FILES、$_SERVERの値を利用する場合に必ず文字エンコーディングをチェックします。常に文字エンコーディングはチェックすべきなので、グローバルなチェック処理の一つとしてmb_check_encoding関数を利用しなければなりません。一部例外が必要な場合─⁠─例えばクロスサイトでデータ交換を行うためにバイナリデータをクエリ文字列にURLエンコードして受け渡すなど─⁠─には例外処理として除外するようにします。

チェックポイント:文字列のエスケープにデータベースAPI関数を利用し、データベース接続リソースも指定しているか?

文字列のエスケープ処理を常に正しく行うには、クエリを送信するデータベース接続で利用している文字エンコーディング情報を利用してエスケープ処理する必要があります。特にMySQLを利用している場合、APIを正しく利用しないと文字エンコーディングベースのSQLインジェクションに脆弱になる可能性が高くなります。

チェックポイント:すべてのパラメータをエスケープしているか?

文字列のエスケープは多少のオーバーヘッドが必要です。すべてのパラメータをエスケープするのは無駄に思えるかもしれませんが、エスケープしないとセカンドオーダーSQLインジェクション(間接SQLインジェクション)の可能性も考慮しなければならなくなるなどの弊害も発生します。このようなリスクとセキュリティチェックの容易性を考慮すると、すべてのパラメータはエスケープするほうがよいと言えます。

チェックポイント:すべてのパラメータを文字列として取り扱っているか?

前のチェックポイントですべてのパラメータがエスケープ処理されているか確認しましたが、すべてのパラメータが文字列として処理されていないと意味がありません。

例えば、uid、gidが整数であるテーブルのエスケープ処理をする場合に

$sql = 'SELECT * FROM user WHERE uid = '.pg_escape_string($_POST['uid']). ' AND gid = '. pg_escape_string($_POST['gid']). ';';

としても意味がありません。$_POST['uid'] に

1; DELETE FROM user; --

などが入っていればSQLインジェクションが可能です。すべての入力を文字列として取り扱えば、このような問題は発生しません。

$sql = "SELECT * FROM user WHERE uid = '".pg_escape_string($_POST['uid']). "' AND gid = '". pg_escape_string($_POST['gid']). "';";

ところで、uid、gidが整数型であるなら整数型にキャストすればよいのでは? と考えられた方もいると思います。しかし、キャストする方法は2つの理由でベストプラクティスとは呼べません。

キャストした場合、不正な攻撃目的の入力が行われてもクエリエラーが発生しない可能性があります。攻撃目的の入力は検出できるほうが好ましく、文字列として扱えばクエリエラーで簡単に攻撃用の文字列が検出できます。またキャストを行うと、32ビットアーキテクチャのコンピュータでは符号付き32ビット整数となり、データベースが一般的にIDに利用する符号付き64ビット整数に比べ著しく狭い範囲の整数でしか正常に動作しません。

チェックポイント:データベースが利用する文字エンコーディングを変更する場合、クエリでなくデータベースAPI関数を利用しているか?

データベース文字エンコーディングをSJISに変更した場合に最も影響が大きいですが、ほかの文字エンコーディングでもセキュリティ上の問題の一因となることもあります。最も分かりやすい、MySQLを例にします。データベース文字エンコーディングはバイナリを利用しているとします。

mysql_query("SET NAMES 'sjis';");
$result = mysql_query("SELECT * FROM user WHERE first_name = '".mysql_real_escape_string($_POST['first_name'])."' AND '".mysql_real_escape_string($_POST['last_name']) "';");

$_POST['first_name']に「表」などの⁠\⁠を含む文字が指定されると

first_name = '表\' 

とエスケープ処理され、サーバ側ではSJISとして処理されます。この結果、2つ目のパラメータ($_POST['last_name'])でSQLインジェクションが可能になります。

チェックポイント:クエリを生成する場合に「すべて」のパラメータを文字列として扱いエスケープ処理し、クエリを生成しているか?

実際時々見かける間違いですが、⁠プリペアードクエリを利用すればSQLインジェクションできない」とするSQLインジェクション対策の原理を理解せずにプリペアードクエリを利用しているプログラマが、プリペアードクエリの生成にパラメータを使ってしまっているケースがあります。

$prepared = "SELECT ". $_GET['field_name'] . " FROM " . $_GET['table_name']. " WHERE user = $1 AND group = $2";

このようなプリペアードクエリの作成ではまったくSQLインジェクション対策になりません。

チェックポイント:テーブル名、フィールド名の指定はホワイトリスト方式か?

テーブル名やフィールド名をパラメータで設定している場合、利用可能な文字を限定する方式も可能ですが、できればホワイトリスト方式でチェックします。

実際の確認手順

1. ソースコード中にSQLインジェクションが可能となる可能性があるコードが無いか?

PHPファイルすべてから⁠SET NAMES⁠(MySQL⁠⁠、⁠SET CLIENT ENCODING⁠(PostgreSQL)などの文字列を検索します。

LinuxやMac OSなどでは以下のようなコマンドで検索できます。

find . -name "*.php" | xargs grep -in "SET NAMES"

2. エスケープ関数が正しく使われているか?

PHPファイルすべてから⁠mysql_real_escape_string⁠, ⁠mysql_escape_string⁠, ⁠pg_escape_string⁠, ⁠pg_escape_bytea⁠, ⁠addslashes⁠関数の利用箇所を探し、不適切な使用方法がないか確認します。mysql_real_escape_string、pg_escape_stringが利用され、データベース接続リソースも指定されていることを確認します。

LinuxやMacOSなどでは以下のようなコマンドで検索できます。

find . -name "*.php" | xargs grep -in "mysql_real_escape_string"

3. クエリ実行箇所を確認し、すべてのパラメータが文字列として扱われエスケープされているか?

PHPファイルすべてから⁠mysql_query⁠, ⁠mysql_unbuffered_query⁠, ⁠mysql_db_query⁠, ⁠pg_query⁠, ⁠pg_send_query⁠関数の利用箇所を探し、クエリ文生成の際にすべてのパラメータが文字列として扱われ適切にエスケープされていることを確認します。テーブル名やフィールド名をパラメータで設定している場合、ホワイトリスト方式でチェックされていることを確認します。

4. プリペアードクエリが利用されている箇所を確認し、プリペアード文にパラメータが含まれないか?

PHPファイルすべてから⁠pg_prepare⁠, ⁠pg_query_params⁠, ⁠pg_send_prepare⁠, ⁠pg_send_query_params⁠関数が利用されている箇所を探し、プリペアード文の生成にパラメータが利用されていないか確認します。テーブル名やフィールド名をパラメータで設定している場合、ホワイトリスト方式でチェックされていることを確認します。

おすすめ記事

記事・ニュース一覧