PHPに簡易WAF機能を追加するのは簡単です。今すぐできます。同じ考え方で他の言語でも実装可能ですし、Apacheのmod_rewirteを使って実装、iptablesのstringモジュールなどを使っても実装できます。
ここで紹介するWAFは簡単な仕様ですが、本格的に拡張することも可能です。
簡易WAFの仕様
Webの一部を除き入力基本的には全てテキストです。攻撃によく利用される文字を除外することにより簡易WAFを実装します。
- $_POST、$_GET、$_COOKIEは全てテキストだと仮定しバリデーションを行う(バイナリは無い)
- テキストの制御文字を検出する
- 検出する制御文字の例外は”\n”,”\r”,”\t”とする
- 文字エンコーディングチェックをする(UTF-8のみ)
- $_SERVER[‘HTTP_*’]も対象とする
たったコレだけです。WAF場合、アプリケーション入力を考慮した検査が行えない(実装できますが、その場合は普通にアプリで入力バリデーションする方が余程効率が良い)のでブラックリスト型の検査を用います。セキュリティ対策では基本的にホワイトリストを用いますが、ブラックリストを利用せざるを得ない例の一つです。
簡易WAFの実装
php_waf() はたったこれだけです。
<?php function php_waf() { // UTF-8以外は処理しない if (strcasecmp('UTF-8', ini_get('default_charset'))) { return; } // ループでmb_check_encodingを回すより、連結の方が速い $raw_data = (array($_GET, $_POST, $_COOKIE)); $data = ''; array_walk_recursive($raw_data, function ($k,$v) use (&$data) { $data .= "$k($v)"; }); $data .= @$_SERVER['HTTP_ACCEPT']; $data .= @$_SERVER['HTTP_ACCEPT_ENCODING']; $data .= @$_SERVER['HTTP_ACCEPT_LANGUAGE']; $data .= @$_SERVER['HTTP_USER_AGENT']; $data .= @$_SERVER['HTTP_REFERER']; // 不正な入力を検出した場合の、動作(HTTPレスポンスコード)、メッセージ(ページ)などは必要に // 応じて追加してください。攻撃者に丁寧なレスポンスは不要ですが、誤検出のケースも想定しなければ // なりません。 // 文字エンコーディングバリデーション if (!mb_check_encoding($data, 'UTF-8')) { trigger_error('Attack detected: Invalid UTF-8 encoding - '. rawurlencode($data), E_USER_ERROR); exit(1); } // 文字列バリデーション if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', $data) > 0) { trigger_error('Attack detected: Disallowed chars detected - '. rawurlencode($data), E_USER_ERROR); exit(1); } } php_waf(); echo 'OK'; // テスト用、運用時には削除/コメントアウトしてください
仕様に関する備考
HTTPアップロードを行う場合、$_FILESが初期化されます。ファイル名などの要素を持ちますがコードをみてわかるように、これらはチェックされません。文字エンコーディングがUTF-8でない場合は実行されません。通常、PHPのログは最大1KBに制限されているので全てのデータをログには保存できません。
あまりお勧めする方法ではありませんが、$_GETなどにはバイナリデータをURLエンコードして利用できます。PHPはデコードした状態で$_GETなどを初期化するので、こういうアプリには利用できません。
配列型データを考慮していません。配列型の入力がある場合、それ用のコードを追加する必要があります。今回はできるたけ簡単にするため対応していません。
$_REQUESTは使わない方が良いグローバル変数です。このため$_REQUESTはチェックしていません。
拡張
$_GETなどの添字に”<“, “>”, “&”などHTMLで危険な文字を利用できます。これらは普通利用されないのでチェックするなどが考えられます。
普通、パラメータ名はアプリケーションで統一しているので
- $idや$xxx_idなどのパラメータ名は数字のみ
- $nameや$xxxx_nameなどのパラメータには記号を許可しない
などの拡張が考えられます。
アプリケーションの設計自体を変える必要がありますが、パラメータ名にハンガリアン記法(変数名にデータ内容のプレフィックスを付ける命名法)を使ってWAFの精度を高めることもできます。例えば、以下のようなルールにします。
- $i_xxx – 数字のみ
- $in_xxx – 環境ネイティブの32/64bit整数型
- $f_xxx – 浮動小数点
- $fn_xxx -環境ネイティブの浮動小数点
- $b_xxx – 論理値、0/1、t/fなど
- $n_xxx – 名前(256バイト以下)
- $ss_xxx – 小さな文字列(64バイト以下)
- $sm_xxx – 中程度文字列(256バイト以下)
- $sl_xxx – 大きな文字列(1024バイト以下)
- $sh_xxx – 巨大な文字列(XXXバイト以下)
- $su_xxx – 無制限文字列
- $p_xxx – バイナリデータ(Passthruのp。サイズも指定した方が良い)
- $a_xxx – 配列型データ
プレフィックスでなく、ポストフィックスでも構いません。命名法は統一していればどのような物でも構いません。
REQUEST_URIやホスト名(仮想ホストの場合)を使った様々なセキュリティ処理を追加することも可能です
簡易WAFで何が防げるのか
拡張しないと簡易WAFで防げる脆弱性はあまりありませんが、追加の防御策としては入れておいても良いでしょう。
- NULL文字インジェクション
- バッファオーバーフローによる任意コード実行
- セキュリティフィルタ回避
- 壊れた文字エンコーディングによる攻撃
PHPのファイル関数、header関数はNULL文字インジェクション対策が行われています。NULL文字インジェクションは予想外の部分で効果がある場合があります。例えば、OracleにはNULL文字を利用したSQLインジェクションフィルタを回避できる問題があります。万が一、バッファーオーバーフローがあった場合でも、サイズ制限/制御文字が使えないと任意コード実行が困難になります。(英数字だけでもシェルコードを作ることもできますが、ハードルは高くなる。そもそも長大な値でないとオーバーフローしない場合も多い)攻撃者はパターンマッチによるセキュリティフィルタを回避する為に制御文字を利用します。フィルタ回避脆弱性はブラックリスト型のフィルタを回避するので、アプリで正しくホワイトリストによるバリデーションをしていれば必須ではありません。
アプリでの入力バリデーションに比べるとかなり見劣りする対策ですが、正しく、漏れ無く、は結構難しいです。こういったアプリのコードに関わらず、全体に適用できるセキュリティ対策はとても有効です。これは多層防御の一つなので他のセキュリティ対策とは独立して利用します。
拡張の部分で紹介したパラメータ名をバリデーションルールとして利用すれば、簡易WAFの精度は飛躍的にあがります。パラメータ名の命名法を考える場合、頭の角に置いておくと良いでしょう。
まとめ
WAFを導入するとしても、この程度の簡易なものであればPHPで実装した物でも十分でしょう。高機能なWAFにはサービス不能攻撃防止など様々な機能が付いていたり、Webサーバーレベルの脆弱性に対応したりするので完全に代用できる物ではありません。
しかし、一台のWAFではなくWebサーバー自体がWAFになるのでスケーラビリティがあります。簡易WAFとは言っても拡張すれば簡単に高機能化できます。
オープンソースアプリを使っているがソースコードの確認はできていない、という場合などにphp.iniのauto_prepend_file設定を使って読み込むと良いでしょう。$_GET/$_POST/$_COOKIEに配列を使っている場合、配列に対応することも忘れないようにしてください。
簡単とは言ってもApacheならmod_rewriteを使った簡易WAFの方がもっと手軽と思います。