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

CodeIgniter の XSS 対策はどうあるべきか? では、ビューでの変数をデフォルトで HTML エスケープするという実装案を示しましたが、それだけで XSS 対策が完了するわけではありません。

今回は、現状の CodeIgniter でどのような XSS 対策をすればいいかについて考えてみます。なお、実際のサイトでは、入力値のバリデーションも必須ですが、ここでは割愛してます。

なお、CodeIgniter 標準の XSS フィルタに対する私の考えは、CodeIgniter ユーザが CodeIgniter の XSS フィルタについて知るべき 5つのこと に記載しています。


XSS 対策として以下のすべてを実施します。

  1. CodeIgniter の文字エンコーディングUTF-8 にする
  2. HTTP レスポンスヘッダの文字エンコーディングを正しく指定する
  3. HTML の属性値はすべてダブルクォート「"」で囲む
  4. ビュー内のすべての変数に対して、コンテクストに応じた正しいエスケープ処理などを施す

1. CodeIgniter の文字エンコーディングUTF-8 にする

config.php でのデフォルトが UTF-8 なので特に設定を変更する必要はありません。

$config['charset'] = 'UTF-8';

2. HTTP レスポンスヘッダの文字エンコーディングを正しく指定する

php.ini で default_charset を UTF-8 に設定するか、以下のコードを CodeIgniter に追加します。

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

どこに追加するかが問題ですが、正統的にはフックの pre_system でしょうか。裏技的には config.php に追加するのもありでしょう。

3. HTML の属性値はすべてダブルクォート「"」で囲む

シングルクォート「'」で囲んだり、クォートを省略したりしないようにコーディングします。

<input name="name" value="abc" />
× <input name='name' value="abc" />
× <input name=name value="abc" />

4. ビュー内のすべての変数に対して、コンテクストに応じた正しいエスケープ処理などを施す

以下のビューを例に説明します。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>...(1)...</title>
	<style type="text/css">...(6)...</style>
</head>

<body onload="foo('...(4)...')">
<div id="container">
	<h1>...(1)...</h1>

	<script type="text/javascript">
		document.write('...(5)...');
	</script>

	<div id="body">
		<p style="...(7)...">...(1)...</p>
		<p>...(1)... <a href="...(2)...">...(1)...</a> ...(1)...</p>
		<p>...(1)...</p>

		<form ...>
		<input name="name" value="...(3)...">
		<input type="submit">
		</form>
	</div>
</div>

</body>
</html>
(1) 要素内容

■ タグを許可しない場合
変数を HTML エスケープします。

以下のような h() 関数を作成し、すべての変数をこの関数を通します。

<?php
function h($var)
{
	if (is_array($var))
	{
		return array_map('h', $var);
	}
	else
	{
		return htmlspecialchars($var, ENT_QUOTES, config_item('charset'));
	}
}

ビューのサンプル

<title><?php echo h($title); ?></title>

(2011/08/26 追記) CodeIgniter 本体に同様の関数が追加されました。詳細は、CodeIgniter に htmlspecialchars() を簡単に利用するための関数が追加されました を参照してください。

■ タグを許可する場合
HTML Purifier で変数を処理します。

CodeIgniter 用のヘルパーの例は、
http://codeigniter.com/wiki/htmlpurifier/
にあります。ここでは purify() というヘルパー関数を作成し HTML Purifier での処理をさせています。

ビューのサンプル

<p><?php echo purify($body); ?></p>
(2) URI の属性値

URI の形式である値のみを出力します。

例えば、以下のようなヘルパー関数を作成し、その関数で処理します。ここでは、

http:
https:

で始まる文字列のみを許可しています。

<?php
function allow_url($str)
{
	if (preg_match('/\Ahttp(s?):/', $str) OR 
		preg_match('#\A/#', $str))
	{
		return h($str);
	}
	else
	{
		$backtrace = debug_backtrace();
		$view = $backtrace[0]['file'];
		$line = $backtrace[0]['line'];

		log_message('error', 
				'allow_url() got unallowable string: "' . $str . '" in ' . $view . 
				' on line ' . $line);
		return 'Error';
	}
}

ビューのサンプル

<a href="<?php echo allow_url($url); ?>"><?php echo h($url); ?></a>
(3) 属性値

すべての変数を HTML エスケープします。具体的には、h() 関数で処理します。

ビューのサンプル

<input name="name" value="<?php echo h($name); ?>">
(4) イベントハンドラ内の文字列リテラル

Unicode エスケープします。

以下のような escape_js_str() 関数を作成し、すべての変数をこの関数を通します。

<?php
// 文字列をすべて \uXXXX 形式に変換
function unicode_escape($matches)
{
	$u16 = mb_convert_encoding($matches[0], 'UTF-16');
	return preg_replace('/[0-9a-f]{4}/', '\u$0', bin2hex($u16));
}

// 英数字とマイナス、ピリオド以外を \uXXXX 形式でエスケープ
function escape_js_str($str)
{
	return preg_replace_callback('/[^-\.0-9a-zA-Z]+/u', 
					'unicode_escape', $str);
}

ビューのサンプル

<body onload="foo('<?php echo escape_js_str($str); ?>')">

このエスケープ方法は、『体系的に学ぶ 安全なWebアプリケーションの作り方』による「JavaScriptの現実的なエスケープ方法」(P.113)です。

(5) script 要素内の文字列リテラル

Unicode エスケープします。

具体的には、(4) と同じくすべての変数を escape_js_str() 関数で処理します。

ビューのサンプル

<script type="text/javascript">
	document.write('<?php echo escape_js_str($str); ?>');
</script>
(6) style 要素内

この位置に変数を入れないといけない必要性をあまり感じませんが、例えば、以下のようなヘルパー関数を作成し通すことで、出力される値を数字やアルファベットなどに制限し、スクリプトが記述できないようにします。

数字だけを許可するヘルパー関数の例。

<?php
function allow_num($str)
{
	$CI =& get_instance();
	$CI->load->library('form_validation');
	
	if ($CI->form_validation->numeric($str))
	{
		return $str;
	}
	else {
		$backtrace = debug_backtrace();
		$view = $backtrace[0]['file'];
		$line = $backtrace[0]['line'];

		log_message('error', 
				'allow_num() got unallowable string: "' . $str . '" in ' . $view . 
				' on line ' . $line);
		return 'Error';
	}
}

アルファベットと数字、アンダースコア(_)、ダッシュ(-)を許可するヘルパー関数の例。

<?php
function allow_alnum($str)
{
	$CI =& get_instance();
	$CI->load->library('form_validation');
	
	if ($CI->form_validation->alpha_dash($str))
	{
		return $str;
	}
	else {
		$backtrace = debug_backtrace();
		$view = $backtrace[0]['file'];
		$line = $backtrace[0]['line'];

		log_message('error', 
				'allow_alnum() got unallowable string: "' . $str . '" in ' . $view . 
				' on line ' . $line);
		return 'Error';
	}
}

ビューのサンプル

<style type="text/css">
	h1 { text-align: <?php echo allow_alnum($str); ?>; color: #<?php echo allow_num($num); ?>; }
	p { line-height: 150%; margin-left: <?php echo allow_num($num); ?>%; }
</style>
(7) style 属性値

(6) と同じく allow_num() や allow_alnum() 関数を通します。

ビューのサンプル

<h1 style="font-size: <?php echo allow_num($num); ?>%; text-align: <?php echo allow_alnum($str); ?>;">見出し</h1>