この記事はCodeIgniter Advent Calendar 2016の15日目です。

xss対策はたいていのアプリケーションで対応しなければいけませんが、CodeIgniter3は標準ではxss対応してくれません。この記事では対応方法について取っ掛かりになるところを提供したいと思います。

CI2から3の流れで対応があいまいに

もともとCodeIgniter2では、xss対応は入力時にフィルタリングをかけていました。この対応はセキュリティ対策としてはあまりよろしくなく、CI3で非推奨となりデフォルトでoffになりました。かといって別の機能が提供されているわけではなく、CI3ではxssの対応は自分で行う必要があります。

debugレベルのログを取ると

Global POST, GET and COOKIE data sanitized

というメッセージが表示されますが、これはヌル文字の削除など”サニタイズ”であってxssフィルタではありません。

デフォルト動作確認

動作を確認してみましょう。コントローラでDBなりPOST値なりからデータを取得し、viewに渡すという想定でfontタグを埋め込んでみます。

<?php

class Sample extends CI_Controller
{
    public function index()
    {
        $this->load->view('sample', array(
            'title' => '<h2>タイトル</h2>',
            'body1' => array(
                'red' => '<font color="red">赤</font>',
                'blue' => '<font color="blue">青</font>',
                'yellow' => '<font color="yellow">黄色</font>',
            ),
            'body2' => (object)array(
                'green' => '<font color="green">緑</font>',
            ),
        ));
    }
}

viewは次のようにします。

<?= $title ?>
<?php foreach ($body1 as $str) : ?>
    <?= $str ?><br>
<?php endforeach ?>
<?= $body2->green ?>

ブラウザから見ると、こうなります。

見事に色がついています。

$this->load->view()を拡張する

この動きを「コントローラからviewに渡した変数は自動でエスケープする」という動きに変更しましょう。それで気を使わなくてもxss対策ができるはずです(注:これだけでは抜けがありますが詳しくは後述します)。

コアの拡張になりますが、viewクラスというものはありません。CI_Loaderのview()メソッドを拡張することにします。

CI_LoaderはCodeIgniterのコア中のコアというべきクラスですが、なんとこれもMY_シリーズで拡張できます。次のようにします。

<?php

require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/BaseEscaper.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/ArrayDecorator.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/Escaper.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/ObjectDecorator.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/IteratorDecorator.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/SafeDecoratorInterface.php');
require_once(APPPATH . 'third_party/output_escaper/src/Symfony/Component/OutputEscaper/SafeDecorator.php');


class MY_Loader extends CI_Loader
{
    public function view($view, $vars = array(), $return = FALSE)
    {
        return parent::view(
            $view,
            array_map(function($v){ return Symfony\Component\OutputEscaper\Escaper::escape('htmlspecialchars', $v); }, $vars),
            $return
        );
    }
}

この例ではsymfony 1系で使われていたOutputEscaperを使用しています。正確にはsymfony 2系向けに作り直されたものの廃止されたものです(代わりにtwigになりました)。

https://github.com/fabpot-graveyard/output-escaper

配列やオブジェクトも自動判定し、必要なときに動的にエスケープしてくれるスグレモノです。必要であればgetRawValue()でエスケープ前の生データを取得することもできます。
ただしスカラ変数はその場でエスケープします。

require_once()の連打はただの手抜きです。本当はautoloaderを設定するべきところです。

これを作ったうえで、もう一度ブラウザで表示させてみましょう。

無事エスケープされました。これでviewで気を使う必要はなくなります。

もうすこし踏み込む:そんな方法で大丈夫か?

大抵の場合はこの方法で十分になりますが、viewを使い倒そうとするとこれでは足りなくなります。

viewに変数を渡す方法は$this->load->view()だけではありません。$this->load->vars()もありますので、こちらもオーバーライドするなど手当てする必要があります。

また、viewから直接DBを見る場合など、コントローラを経由しない変数には通用しません。OutputEscaperの限界がここにあります。

コントローラを経由しない変数まできちんとしたいならば、Smartyやtwigなどテンプレートエンジンを入れる必要が出てきます。テンプレートエンジンを入れる場合、$this->load->view()を拡張するか、別のメソッドを作ると良いと思います。

どのテンプレートエンジンを入れるかについては、今はこれという決め手はないように思います。Smartyは一部のフロントエンドエンジニアから蛇蝎のごとく嫌われていて、またtwigは日本語情報が不足しがちという問題があります。
個人的にはSmartyを推します。Smartyはver3で字句解析器・構文解析器が導入され、文法がだいぶキレイになりました。数年触っていない方は再評価されても良いと思います。ver3は2010年11月のリリースです。

開発中のCodeIgniter4にはview parserがあります。自動エスケープという観点からはイマイチな感もあるのですが、デフォルトで提供されているというのは強みになるかもしれません。


次は18日目の記事に飛びます(2016-12-19時点)。