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

第11回スクリプトインジェクションを防ぐ10のTips

前回はスクリプトインジェクションがなくならない理由を紹介しました。それをふまえて今回はスクリプトインジェクションを防ぐ10のTipsを紹介します。

デフォルト文字エンコーディングを指定

php.iniには、PHPが生成した出力の文字エンコーディングをHTTPヘッダで指定するdefault_charsetオプションがあります。文字エンコーディングは必ずHTTPヘッダレベルで指定しなければなりません。しかし、デフォルト設定ではdefault_charsetが空の状態で、アプリケーションで設定しなければ、HTTPヘッダでは文字エンコーディングが指定されない状態になります。

HTTPヘッダで文字エンコーディングを指定しない場合、スクリプトインジェクションに脆弱になる場合あるので、default_charsetには⁠UTF-8⁠を指定することをお勧めします。サイトによってはSJIS、EUC-JP、ISO-2022-JPなどを利用していると思います。これらのサイトでUTF-8に設定すると文字化けが発生する可能性があります。しかし、default_charset="UTF-8"を設定して文字化けが発生する場合、アプリケーション側で文字エンコーディングを設定していないことになり修正が必要であることが分かるので、問題を未然に防ぐことができます。

UTF-8をお勧めするには理由があります。例えば、EUC-JP文字エンコーディングを利用しているサイトでSJISをデフォルト文字エンコーディングに設定すると、アプリケーションによってはスクリプトインジェクションが可能になります。UTF-8なら安全、とは言えませんが、ほかの日本語文字エンコーディングよりは安全なデフォルト設定だと言えます。

アプリケーションを作る場合、必ず文字エンコーディングを明示的にスクリプトから設定するようにします。

ini_set('default_charset', 'UTF-8');

アプリケーションで文字エンコーディングを設定している場合でも、万が一設定漏れがあった場合に備えてphp.iniやWebサーバの設定ファイルでアプリケーションが使用しているデフォルト文字エンコーディングを指定したほうがよいです。以下の設定はApache Webサーバのhttpd.confや.htaccessで利用できます。

php_value default_charset "UTF-8"

文字エンコーディングを入力時にチェック

すべてのWebアプリケーションはブラウザなどから入力を受け取った際に、必ず文字エンコーディングが正しいかチェックしなければなりません。文字エンコーディングが壊れていないかチェックしないと、思いもよらない箇所で問題が発生する場合があります。例えば、PHP5.2.5では壊れたUTF-8文字エンコーディングを利用しhtmlentities、htmlspecialcharsによるエスケープを回避できる問題が修正されました。文字エンコーディングが正しいかチェックし、不正な場合はセキュリティエラーとしてスクリプトの実行を停止していれば、htmlentities/htmlspecialcharsに脆弱性がある状態でも安全にこれらの関数を利用できます。

Webアプリケーションから入力されたデータはWebアプリケーションのみでなく、ほかのアプリケーションなどでも利用される場合も少なくありません。ほかのアプリケーションがデータを安全に処理するためにも、文字エンコーディングのチェックは欠かせません。

文字エンコーディングのチェックにはmb_check_encoding関数を利用します。PHP4とPHP5両方で利用できます。

入力文字列は厳格にバリデーション

よくあるインジェクション系の脆弱性の原因は入力チェック不足であることがほとんどです。データベースレコードIDが整数であるにも関わらず0から9の文字のみで構成されているかチェックしていない等です。日本国内の電話番号なら1,2,3,4,5,6,7,8,9,0,-のからなる12または13文字になります。

すべての入力はアプリケーションが期待している文字、形式および長さを満たしているかチェックしなければなりません。⁠バリデーションはインジェクション対策でない」といった意見を稀に聞きます。しかし、厳格な入力バリデーションが行われていないアプリケーションの安全性検証は、厳格な入力バリデーションが行われているアプリケーションに比べて非常に困難です。

厳格な入力のバリデーションはすべてのセキュリティ対策の基本です。必ず厳格な入力バリデーションを行うようにします。

出力する場合はエスケープした出力をデフォルトに設定

アプリケーションではechoやprint文など、文字列を直接出力する関数は利用しないようにします。簡単なラッパー関数を作成し、デフォルトではエスケープして出力するようにします。

define('ESCAPE_HTML', 1);
define('ESCAPE_URL', 2);
define('NOESCAPE', 3);

function p($str, $escape=ESCAPE_HTML, $encoding='UTF-8') {
   switch ($escape) {
      case ESCAPE_HTML:
         echo htmlentities($str, ENT_QUOTES, $encoding);
         break;
      case ESCAE_URL:
         echo rawurlencode($str);
         break;
      case NOESCAPE:
         echo $str;
         break;
      default:
         trigger_error('Invalid option');
         exit;
   }
}

p('<script>alert(1)</script>'); // デフォルトではHTMLエスケープされる

この考え方はWebアプリケーションのみでなく多くのアプリケーションに取り入れられています。HTMLを使用しているのはWebブラウザだけではありません。SkypeなどのクライアントアプリケーションでもメッセージがHTMLタグで記述されており、デフォルトでエスケープする仕様に変更された際にタグがそのまま見えていた、などの事例はよくあります。Webアプリケーション開発フレームワークなどでもすべての出力はデフォルトでHTML用にエスケープされるフレームワークもあります。

エスケープせずに出力する場合、確実に安全であることを確認

100%の安全性を保証できる場合にのみエスケープ無しで出力します。例えば、データベースからの取得した整数型のデータであっても、データが本当に整数型であるかは値をチェックしないと保証できません。データベースによっては整数型のコラムでも文字列が保存可能なデータベースもあります。整数型のコラムと思っていたコラムが文字列型であるかも知れません。プログラム中でいちいちこのような心配をするのは面倒です。整数型であっても、ひとつ前のTipのようにエスケープしてしまうほうが安全で確実です。

ブラウザがどのような文字列でJavaScriptを実行するかはブラウザの実装により異なります。下記のXSS Cheat Sheetを参照すると、直観的にはJavaScriptを実行できるとわかりづらい文字列でもJavaScriptを実行してしまうことがわかります。

XSS Cheat Sheet
http://ha.ckers.org/xss.html

strip_tags関数では許可タグを設定しない

PHPにはHTMLタグを削除してくれるstrip_tagsと呼ばれる関数があります。この関数には許可するHTMLタグを指定できますが、タグを許可した場合、そのタグの属性もすべて残ったままになります。つまり、strip_tags関数で許可するタグを指定した場合、関数の戻り値をそのまま出力するとスクリプトインジェクションに脆弱になります。

例えば<b>タグを許可した場合、

<b onmouseover="javascript: alert(1)">text</b>

などと記述されていると簡単にJavaScriptが実行できます。

許可するタグを指定したい場合はDOMやSAXを利用します。

HTMLのパースに正規表現は利用しない

ユーザから送信されたテキスト中に含まれるタグをエスケープしないで出力する場合、許可するタグとそのタグで許可する属性を抽出しなければなりません。タグと属性を抽出するためにはテキストをパースしなければなりません。最も手軽な方法は正規表現を利用しテキストをパースする方法です。しかし、正規表現を利用してテキストを適切にパースするのは簡単ではありません。HTMLをパースする場合、XMLやDOMを処理するモジュールを利用します。

タグや属性を出力する場合は厳格なホワイトリストで確認

タグや属性を出力する場合、許可するタグとその属性と値を明確に定義します。特に属性値は厳格に処理しないとJavaScriptなどの混入を許してしまう場合があります。

許可するタグや属性と値は基本的にすべてホワイトリスト形式でチェックします。例えば、ユーザがCSSスタイル名を指定できるようにする場合、ユーザが指定可能なスタイル名を配列に保存し、配列に保存されたスタイル名と一致するかどうかチェックします。任意の文字列を処理したい場合は可能な限り限定した文字列のみ許可するようにします。

CSSファイル生成に注意を払う

CSSファイルの生成にPHPを利用する場合、JavaScriptが実行されないように注意しなければなりません。ユーザ入力値をCSSファイル生成に利用する場合、タグや属性と値をスクリプトから設定するのと同じようにホワイトリスト方式でユーザ入力値をチェックします。

JavaScript生成に注意を払う

PHPでJavaScriptファイルを生成する場合、ユーザ入力をそのまま出力すると任意のJavaSriptの実行が可能になります。PHPでJavaScriptを出力する場合には、文字列、コンテクストに応じてホワイトリスト方式でのチェックやエスケープを行ってから出力しなければなりません。ユーザ入力を利用したJavaScriptの生成はできる限り行わないほうがよいでしょう。

おすすめ記事

記事・ニュース一覧