CodeIgniter の XSS 対策はどうあるべきか?

CodeIgniter での XSS 対策については、いろいろ議論があるはずですが、あまり公開された日本語の情報はありません。

CodeIgniter には XSS フィルタリング機能が標準で用意されていますが、これは本質的にブラックリストでありフィルタリング漏れが生じる可能性が残ります。また、実際、過去に脆弱性が指摘されており、CodeIgniter 2.0.1 以前の XSS フィルタにも脆弱性があることが知られています。

また、ビューでの出力時に変数の値をデフォルトですべて文字参照エスケープするという機能はありません。この機能はあるといいと思いますが、どう実装すべきかがそんなに簡単ではないように思われます。

以下の記事に、デフォルトエスケープの実装がみられますが、少々、独特過ぎる感じがします。

今回は、ビューでの変数をデフォルトですべてエスケープするというサンプル実装をしてみました。あまりテストされていませんので使用される場合は、十分に検証するようお願いします。

(2011/06/21) XSS 対策全般に関する記事 CodeIgniter の XSS 対策はどうあるべきか? その2 を追加しました。

実装案

実装方法としては、

  1. ビューに渡す変数をオブジェクトに変更する
  2. オブジェクト出力時は htmlspecialchars() をかけエスケープする
  3. エスケープしない場合は get_raw() メソッドを使う
  4. ただし、配列がビューに渡された場合、配列のキーはエスケープされ、エスケープしないメソッドは使えない

としました。

以下は 2.0.3-hg-tip に対するパッチです。

まず、config.php に $config['view_default_escaping'] を追加します。デフォルトエスケープを有効にする場合は TRUE を指定するようにします。

--- a/application/config/config.php	Sun May 08 11:06:44 2011 -0400
+++ b/application/config/config.php	Mon Jun 06 17:58:26 2011 +0900
@@ -283,6 +283,16 @@
 
 /*
 |--------------------------------------------------------------------------
+| Default HTML Escaping in Views
+|--------------------------------------------------------------------------
+|
+| Determines whether escaping all variables in views is active
+|
+*/
+$config['view_default_escaping'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
 | Cross Site Request Forgery
 |--------------------------------------------------------------------------
 | Enables a CSRF cookie token to be set. When set to TRUE, token will be

htmlspecialchars() を楽に使うための h() 関数を追加しておきます。手動でロードする必要がないように Common.php に追加してます。

--- a/system/core/Common.php	Sun May 08 11:06:44 2011 -0400
+++ b/system/core/Common.php	Mon Jun 06 17:58:26 2011 +0900
@@ -290,6 +290,29 @@
 // ------------------------------------------------------------------------
 
 /**
+* Returns HTML escaped variable
+*
+* @access	public
+* @return	mixed
+*/
+if ( ! function_exists('h'))
+{
+	function h($var)
+	{
+		if (is_array($var))
+		{
+			return array_map('h', $var);
+		}
+		else
+		{
+			return htmlspecialchars($var, ENT_QUOTES, config_item('charset'));
+		}
+	}
+}
+
+// ------------------------------------------------------------------------
+
+/**
 * Error Handler
 *
 * This function lets us invoke the exception class and

ローダクラスを変更し、ビューへ渡す変数をオブジェクトに変更します。また、ビューへ渡すオブジェクトのクラス CI_View_Var クラスを定義します。

--- a/system/core/Loader.php	Sun May 08 11:06:44 2011 -0400
+++ b/system/core/Loader.php	Mon Jun 06 17:58:26 2011 +0900
@@ -702,8 +702,21 @@
 		{
 			$this->_ci_cached_vars = array_merge($this->_ci_cached_vars, $_ci_vars);
 		}
-		extract($this->_ci_cached_vars);
-
+		
+		// If config 'view_default_escaping' is TRUE, give objects to view file to
+		// escape all variables data by default.
+		if (config_item('view_default_escaping'))
+		{
+			foreach ($this->_ci_cached_vars as $key => $val)
+			{
+				$$key = $this->_ci_set_view_object($val);
+			}
+		}
+		else
+		{
+			extract($this->_ci_cached_vars);
+		}
+		
 		/*
 		 * Buffer the output
 		 *
@@ -765,6 +778,33 @@
 	// --------------------------------------------------------------------
 
 	/**
+	 * Create Objects used in views
+	 *
+	 * This function is used to HTML escape all variables in views.
+	 *
+	 * @param	mixed
+	 * @return	object CI_View_Var
+	 */
+	protected function _ci_set_view_object($var) {
+		if (is_array($var))
+		{
+			$new_array = array();
+			foreach ($var as $key => $val)
+			{
+				$new_key = h($key);
+				$new_array[$new_key] = $val;
+			}
+			return array_map(array($this, '_ci_set_view_object'), $new_array);
+		}
+		else
+		{
+			return new CI_View_Var($var);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
 	 * Load class
 	 *
 	 * This function loads the requested class.
@@ -1142,6 +1182,87 @@
 		}
 	}
 }
+// END CI_Loader Class
+
+/**
+ * View Variable Class
+ *
+ * To be used as variables in view files for HTML escaped output
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @author		ExpressionEngine Dev Team
+ * @category	Loader
+ * @link		http://codeigniter.com/user_guide/libraries/loader.html
+ */
+class CI_View_Var {
+	public $val;
+	
+	/**
+	 * Constructor
+	 *
+	 * Sets the variable to property
+	 */
+	public function __construct($var)
+	{
+		$this->val = $var;
+	}
+	
+	/**
+	 * return HTML escaped value
+	 * 
+	 * @access	public
+	 * @return	string
+	 */
+	public function __toString()
+	{
+		return h($this->val);
+	}
+	
+	/**
+	 * Fetch raw data
+	 * 
+	 * @access	public
+	 * @return	string
+	 */
+	public function get_raw()
+	{
+		return $this->val;
+	}
+	
+	/**
+	 * Unicode Escaping for JavaScript
+	 * 
+	 * This method is used by escape_js() method.
+	 * 
+	 * @access	protected
+	 * @param	array
+	 * @return	string
+	 */
+	protected function unicode_escape($matches)
+	{
+		$u16 = iconv(config_item('charset'), 'UTF-16', $matches[0]);
+		return preg_replace('/[0-9a-f]{4}/', '\u$0', bin2hex($u16));
+	}
+	
+	/**
+	 * Escape for JavaScript Generation
+	 * 
+	 * This method is used to escape JavaScript dynamic generation.
+	 * Use variables inside <script> tag or event handler like
+	 * inside <body onload="...">.
+	 * Thanks to #wasbook <http://www.hash-c.co.jp/wasbook/>.
+	 * 
+	 * @access	public
+	 * @return	string
+	 */
+	public function escape_js()
+	{
+		return preg_replace_callback('/[^-\.0-9a-zA-Z]+/u', 
+						array($this, 'unicode_escape'), $this->val);
+	}
+}
+// END CI_View_Var Class
 
 /* End of file Loader.php */
 /* Location: ./system/core/Loader.php */

フォームヘルパーには、値をエスケープする form_prep() 関数がありますので、そのままだと二重にエスケープがかかります。デフォルトでエスケープをする場合は、処理をスキップするように変更します。

--- a/system/helpers/form_helper.php	Sun May 08 11:06:44 2011 -0400
+++ b/system/helpers/form_helper.php	Mon Jun 06 17:58:26 2011 +0900
@@ -618,6 +618,12 @@
 	{
 		static $prepped_fields = array();
 
+		// If config 'view_default_escaping' is TRUE, skip prepping
+		if (config_item('view_default_escaping'))
+		{
+			return $str;
+		}
+		
 		// if the field name is an array we do this recursively
 		if (is_array($str))
 		{

上記のコードが見づらいという方は、bitbucket をご覧ください。

使い方

これで、config.php

$config['view_default_escaping'] = TRUE;

と設定し、ビューファイルで普通に

<?php echo $var; ?>

とすれば、エスケープされた値が出力されます。

エスケープしたくない場合は、

<?php echo $var->get_raw(); ?>

とします。

もっと、こうした方がいいというご意見がありましたら、お願いします。