PHPアプリケーションWAFα版(仮称)
特徴
PHPで作ったWAFのような動作をするスクリプトです。
ユーザから送られてくる次のパラメータに対して、浄化処理を行い下記の攻撃や問題を防ぎます。
- Cross site scripting
- NULL Byte Attack
- CRLF Injection もしくは(HTTP Response Splitting)
- Buffer over flow
- マルチバイト問題
- Directory traversal
- PHPの設定ミス(この機能は作り途中でごく一部のみ)
機能
PHPでWebアプリ作るときに、セキュリティ対策を行うのは面倒です。そんな訳で、ユーザから送られてくる殆ど全てのパラメータに対して、勝手に且つ強引に浄化処理を行い攻撃や問題を防ぎます。基本的には、脆弱に成り得る文字を取り除いたり、無害化したりといった処理を行います。対象となるのは、GETパラメータ($_GET)、POSTパラメータ($_POST)、Cookieパラメータ($_COOKIE)、ヘッダパラメータ?($_SERVER['HTTP_からはじまるもの'])に対して、パラメータの浄化処理を行って、それぞれのパラメータを上書きします。上書きを行うので、既存のアプリケーションにも利用できるかと思います。
脆弱なコードと最も簡単な使い方
会員登録のアプリケーションを例にしたCross site scriptingとNULL Byte Attackに対して脆弱なコードの典型的な例。
<html> <form method="GET" action="hoge.php"> <input type="text" name="name"> <input type="text" name="tel"> <input type=submit> </form> <? print $_GET['name'] . "さん。"; //数字のみのデータなら if(ereg("^\d+$",$_GET['tel'])){ //登録処理 print "あなたの電話番号" . $_GET['tel'] . "の登録が完了しました!"; }else{print "エラー数字以外が入力されてます。";} ?>
このようなコード場合、
htt://XXXX/hoge.php?name=<script>alert('XSS');</script>&tel=090000%00<script>alert('XSS');</script>
とアクセスすることで、Cross site scripting攻撃が成功し、実際にはアラートが表示され、電話番号のチェック=数字のみかをチェックしている箇所も、NULL Byte Attack攻撃が成功し意味をなさなくなります。
<? require_once("PHP_WAF.php"); $obj = new PHP_WAF("SJIS");//オブジェクト作る、引数に文字コード $obj->filterAll();//サニタイズを行う ?> <html> <form method="GET" action="hoge.php"> <input type="text" name="name"> <input type="text" name="tel"> <input type=submit> </form> <? print $_GET['name'] . "さん。"; //数字のみのデータなら if(ereg("^\d+$",$_GET['tel'])){ //登録処理 print "あなたの電話番号" . $_GET['tel'] . "の登録が完了しました!"; }else{print "エラー数字以外が入力されてます。";} ?>
この様に頭に3行足すことで、アプリはPHP_WAFによってパラメータがHTMLエンコードされ、NULLバイトを取り除かれる事によって脆弱性の対策がなされます。
お願い
バグ、機能に関しての連絡。ここのコードおかしくない? ハックできたよheheheって連絡ぜひください。連絡先は下にあります。
注意
上記挙げた脆弱性も含めて万能ではありません。このプログラムはまだアルファバージョンです。
使い方
//ここからのコードは必ずスクリプトの一番最初に書いてください。 //ファイルを読み込みますコードはブログの下の方にあります。 require_once("PHP_WAF.php"); //オブジェクト生成、ユーザの入力値の文字コードをセットしてください。 $obj = new PHP_WAF("SJIS"); //サニタイズ処理を全く行いたくない、又は一部の対策のみ行いたいパラメータを指定します。 //例はGETパラメータの hoge は改行文字を入れたいから除外する。 $obj->exceptParam("GET","hoge"); //パラメータにサニタイズ処理を行う(exceptParamで除外したものを除く) //1-5番目の引数で指定した対策を行う項目名(BOF,XSS,CRLF,NULLBYTE)のいずれか。 //ENCODEはマルチバイト問題、CRLFはCRLF Injection、NULLBYTEは、NULL Byte Attack //BOFはBuffer over flow、XSSはCross site scriptingの対策を行います。 //例では全部やる $obj->sanitizeAllPalam(); //パラメータと対策方法を指定してサニタイズを行う、 //1番目の引数:パラメータの種類(GET,POST,COOKIE,HEAD) //2番目の引数:パラメータの名前 //3-7番目の引数:対策を行う項目を指定(ENCODE,CRLF,NULLBYTE,BOF,XSS) //ここで上で除外したhogeパラメータにBuffer over flowとCross site scriptingの対策を行う。 $obj->sanitizeEachPalam("GET","hoge","BOF","XSS"); //ここまでのコードは必ずスクリプトの一番最初に書いてください。
コード(PHP_WAF.php と名前をつけてあげて下さい。)
<? class PHP_WAF{ /** * 入力文字コード * @var string */ var $_encode; /** * 除外するGETパラメータ * @var array */ var $_exParamGet = array(); /** * 除外するPOSTパラメータ * @var array */ var $_exParamPost = array(); /** * 除外するCookieパラメータ * @var array */ var $_exParamCookie = array(); /** * 除外するHeadパラメータ * @var array */ var $_exParamHead = array(); /** * パラメータの最大入力文字数。マルチバイトも1文字として数えます。 * @var Int */ var $_maxParamLen; /** * コンストラクタ * @param1 string パラメータの文字コード * @param2 Int パラメータの最大文字数(マルチバイトも1文字として数える) */ function PHP_WAF($encode = "",$maxParamLen = 500){ //指定エンコードがサポートされているか if(array_search($encode,mb_list_encodings()) !== FALSE){ $this->_encode = $encode; }else{ die("ERROR"); } //パラメータの最大入力文字数をセット(マルチバイトも1文字として数える) $this->_maxParamLen = $maxParamLen; } /************************************************************************************** メインの処理を行うメソッド4つです。 PHP_WAF::exceptParam 対策を行わないパラメータを指定する。 PHP_WAF::sanitizeAllPalam GET,POST,COOKIE,HEAD全てに指定したサニタイジングを行う。 PHP_WAF::sanitizeEachPalam 指定したパラメータのみ指定したサニタイジングを行う。 PHP_WAF::_sanitize 対策を行うメソッドをコールして /************************************************************************************** /** * sanitizeAll を行った際に除外するパラメータ * notice:sanitizeAll より前にコールされなければなりません。 * 指定されたパラメータは何も対策されません。 * 除外したパラメータは sanitizeEachPalam で * 個別に対策してあげてください。 * @access public * @param1 string パラメータのタイプ(GET,HEAD,POST,COOKIE)のいずれか * @param2 string パラメータ名 * @return Boolean */ function exceptParam($type="",$name=""){ switch($type){ case 'HEAD': $name = "HTTP_" . strtoupper(str_replace("-","_",$name)); $this->_exParamHead[] = $name; break; case 'GET': $this->_exParamGet[] = $name; break; case 'POST': $this->_exParamPost[] = $name; break; case 'COOKIE': $this->_exParamCookie[] = $name; } } /** * 機能:GET,POST,HEAD,COOKIE にサニタイジングを行う * notice: exceptParam より後にコールされるべきです。 * _exParamGet _exParamPost _exParamCookie * _exParamHeadの値のパラメータはスキップされる。 * @param string 対策を行う項目名、のいずれか。 * @access public * @return nothing */ function sanitizeAllPalam($pattern="NULLBYTE,CRLF,ENCODE,BOF,XSS,DIRT"){ //GETパラメータ全てに対策 foreach($_GET as $key => $value){ //除外パラメータでは無いかチェック if($value != "" AND array_search($key, $this->_exParamGet) === FALSE){ //$patternの対策を全て行い上書きする。 $_GET[$key] = $this->_sanitize($value,$pattern); } } foreach($_POST as $key => $value){ if($value != "" AND array_search($key, $this->_exParamPost) === FALSE){ $_POST[$key] = $this->_sanitize($value,$pattern); } } foreach($_COOKIE as $key => $value){ if($value != "" AND array_search($key, $this->_exParamCookie) === FALSE){ $_COOKIE[$key] = $this->_sanitize($value,$pattern); } } foreach($_SERVER as $key => $value){ if($value != "" AND array_search($key, $this->_exParamHead) === FALSE AND preg_match("/^HTTP_/",$key)){ $_SERVER[$key] = $this->_sanitize($value,$pattern); } } } /* * @function GET,POST,HEAD,COOKIE にサニタイジングを行う * @notice sanitizeAllPalam より後にコールされるべきです。 * @param1 string パラメータの種類(GET,POST,COOKIE,HEADのいずれか) * @param2 string パラメータの名前 * @param3 string 対策を行う項目名(BOF,XSS,CRLF,NULLBYTE,ENCODE,DIRT)のいずれか。 * @access public * @return Boolean */ function sanitizeEachPalam($type="",$name="",$pattern=""){ switch($type){ case 'HEAD': $name = "HTTP_" . strtoupper(str_replace("-","_",$name)); $_SERVER[$name] = $this->_sanitize($_SERVER[$name],$pattern); break; case 'GET': $_GET[$name] = $this->_sanitize($_GET[$name],$pattern); break; case 'POST': $_POST[$name] = $this->_sanitize($_POST[$name],$pattern); break; case 'COOKIE': $_COOKIE[$name] = $this->_sanitize($_COOKIE[$name],$pattern); } } /** * param1の値をparam2で指定したメソッドの引数に与える。 * @param1 文字列 * @param2 メソッド名(,で複数指定可能) * @access private * @return サニタイジングされた値 */ function _sanitize($str="",$pattern=""){ $patternArry = preg_split("/,/",$pattern); foreach($patternArry as $key => $method){ if(is_callable(array($this,$method))){ $str = call_user_func(array($this,$method),$str); } } return $str; } /************************************************************************************** ここからは、個別の対策を行うメソッドです。 クラスを継承して、個別の対策を上書きOR追加することが簡単にできます。 個別の対策を行うメソッド名はそのまま PHP_WAF::_sanitize が呼び出すのに使用します。 よって、メソッド名HOGEを作った場合、PHP_WAF::sanitizeAll('HOGE') とすれば良いのです。 文字列入力→サニタイズ→文字列出力の機能を守ってください。 /************************************************************************************** /** * BOF対策を行う。_maxParamLen で指定した長さ(デフォルト500文字)以上は切り捨てる。 * Notice 長さはマルチバイトも1として数えられます。 * @access private * @param string * @return string */ function BOF($str){ return mb_substr($str,0,$this->_maxParamLen,$this->_encode); } /** * XSS対策を行う。 * '"<>& の5つの文字をHTMLエンコードする * @access private * @param string * @return string */ function XSS($str){ return htmlspecialchars($str,ENT_QUOTES,$this->_encode); } /** * crlf対策を行う。 * 改行文字を取り除く * @access private * @param string * @return string */ function CRLF($str){ $chrs = array("\r","\n"); return str_replace($chrs,"",$str); } /** * ヌルバイトアタック対策を行う。NULL文字を取り除く * @access private * @param string * @return string */ function NULLBYTE($str){ return str_replace("\0","",$str); } /** * マルチバイト問題対策を行う。入力文字の再エンコードを行う。 * @access private * @param string * @return string */ function ENCODE($str){ return mb_convert_encoding($str, $this->_encode, $this->_encode); } /** * ディレクトリとらさば 対策を行う。../ ..\ の文字を全角にする。 * @access private * @param string * @return string */ function DIRT($str){ if(preg_match("/\.+[\/\\\]+/",$str)){ $str = str_replace("/","/",$str); $str = str_replace(".",".",$str); $str = str_replace("\\","¥",$str); } return $str; } /************************************************************************************** メインの処理を行うメソッド4つです。 PHP_WAF::exceptParam 対策を行わないパラメータを指定する。 PHP_WAF::sanitizeAll GET,POST,COOKIE,HEAD全てに指定したサニタイジングを行う。 PHP_WAF::sanitizeEachPalam 指定したパラメータのみ指定したサニタイジングを行う。 PHP_WAF::_sanitize 対策を行うメソッドをコールする /************************************************************************************** /** * PHP の設定をセキュアに変更 * notice:ポリシーは著者の独断と偏見です。ここで設定できるのはアクセスレベルがPHP_INI_ALL(7)のものだけです。 * @access public * @param 3 もしくは safe ポリシー:不要と思われるもの全てをOFFに変更 + レベル2,1の項目 * 2 もしくは normal ポリシー:最低限不要と思われるものをOFFに変更 + レベル1の項目 * 1 もしくは min (デフォルト) ポリシー:セキュリティ上脅威とり得るものを設定変更 * @return boolean TRUE もしくは STRING(最後に失敗した項目名) */ function setiniLevel($level = 1){ $failed = array(); if($level == 3 OR $level == "safe"){ //session.auto_start //expose_php=off //file_uploads=off //session.use_strict_mode=1 } if($level >= 2 OR $level == "safe" OR $level == "normal"){ //session.use_trans_sid //if(!ini_set("session.use_trans_sid",0)){$failed[] = "session.use_trans_sid";} //session.use_only_cookies //if(!ini_set("session.use_only_cookie",1)){$failed[] = "session.use_only_cookie";} //session.gc_maxlifetime //memory_limit //safemode //variables_order="GPCS" //register_long_arrays=off //register_argc_argv=off //magic_quotes_gpc=off //allow_url_include=off //if(!ini_set("allow_url_include",0)){$failed[] = "allow_url_include";} } if($level >= 1 OR $level == "safe" OR $level == "normal" OR $level == "min"){ //SQL特殊文字のエスケープを自動で行わない(magic_quotes_gpcは問題がある) //if(!ini_set("magic_quotes_gpc",0)){$failed[] = "magic_quotes_gpc";} //allow_url_fopen //エラーをLogに取るか //if(!ini_set("log_errors",1)){$failed[] = "log_errors";} //取得するエラーのタイプ if(!ini_set("error_reporting",2047)){$failed[] = "error_reporting";} //エラー出力をしない if(!ini_set("display_errors",0)){$failed[] = "display_errors";} //起動時のエラー出力をしない //if(!ini_set("display_startup_errors",0)){$failed[] = "display_startup_errors";} //Content-type:ヘッダの文字コード //if(!ini_set("default_charset",$this->_encode)){$failed[] = "default_charset";} //register_globals //if(!ini_set("register_globals",0)){$failed[] = "register_globals";} } //ini_set に失敗すれば最後に失敗した項目名を返します。 //全て成功時にTRUEを返す。通常のIFの使い方ではなく === TRUE という使い方をして下さい。 //アクセスレベルはバージョンに依存するので結構失敗します。 if(!isset($failed[0])){ return TRUE; }else{ return $failed; } } /** * PHP の設定をユーザにより変更、setiniLevel より後にコールすればsetiniLevelの設定の上書きが可能 * notice:setiniLevel をコールする場合、setiniLevelより後にコールされるべきです。 * ここで設定できるのはアクセスレベルがPHP_INI_ALL(7)のものだけです。 * @access public * @param string 設定項目名 * @param string 設定項目値 * @return boolean (成功時にTRUE 失敗時にFalse) */ function setEachini($name="",$value=""){ return ini_set ($name,$value); } } ?>
連絡先 sekine_works@yahoo.co.jp
注意事項
このソフトウェアを頒布または転載するときは以下の点を遵守してください。オリジナルのまま、頒布または転載してください。頒布または転載の際に、メディア代、通信代等の実費を除いて、金銭の授受があってはなりません。商業的目的で頒布する際には、事前に著作者の許可を取る必要があります。なお、雑誌への掲載収録に限り、許可が不要となります。アルファ版やベータ版など、このソフトウェアがテスト公開バージョンの場合は、再配布できません。このソフトウェアは雑誌へ掲載、収録を歓迎しております。なお、雑誌ではなく書籍形態のものについては、事前に著作者の許可を取る必要があります。著作権は さいばーと が所有しています。このソフトウェアに含まれる著作権情報を書き換えてはなりません。このソフトウェアの使用または使用不可によって、いかなる問題が生じた場合も、著作者はその責任を負いません。バージョンアップや不具合に対する対応の責任も負わないものとします。この文書の内容およびソフトウェアの意匠、仕様は、予告なしに変更されることがあります。