PHPで簡単に配列の値をエスケープ処理する方法

PHPクロスサイトスクリプティング

クロスサイトスクリプティング(XSS)を発生しないようにするには出力時に適切にエスケープ処理させるというのはウェブアプリケーションを開発する人たちにとっては今は常識となっています。

しかし、PHPのように、HTMLに埋め込んで利用できるような言語ではつい「うっかり」エスケープ処理を忘れてしまいXSSが..ということがあります。

CakePHPsymfonyなどのフレームワークを使っていればフレームワーク側で適切に処理させる事が可能ですが、そういったフレームワークを使っていないときにできるだけ簡単にエスケープ処理する方法の1つを紹介したいと思います。

最終的なサンプル

どうやって実現するかは後にして実際のサンプルでどのような動作になるかをみてみましょう。

<?php
$str =  '<script>alert("test")</script>';

$tpl_values = array();
$tpl_values['attack'] = $str;
echo $tpl_values['attack'];

このスクリプトを実行すると以下のように表示されます。

<script>alert("test")</script>

もちろん、途中で何の処理もしていないので当然の結果です。

では、次のコードを見てみてください。

<?php
$str =  '<script>alert("test")</script>';

$tpl_values = secure_array();
$tpl_values['attack'] = $str;
echo $tpl_values['attack'];

さきほどと異なるのはarray()の代わりにsecure_array()という自前関数を呼んでいるだけです。
しかし、これを実行すると

&lt;script&gt;alert(&quot;test&quot;)&lt;/script&gt;

となるのです。
通常と同じ配列操作を行っているだけなのにエスケープさせることができています。
配列操作のコードに変更を加えなくてもエスケープさせることができていることがポイントですね。
しかも、PHP4では不可能な実装です。

では、どうやってこのような実装を行ったかを説明します。

便利なArrayObject

既に推測された方もいるかもしれませんが、ArrayObjectというPHP5から利用できるクラスを利用しています。
ArrayObjectはSPL(standard php library)の1つです。SPLとはPHPをより簡単にオブジェクト指向的プログラムをできるように標準で用意されたライブラリ郡です。
PHP: SPL - Manual
PHP: ArrayObject - Manual

このArrayObjectを拡張したクラスは、まるで配列と同じようなアクセスが可能なオブジェクトを作成する事が可能になります。
ブラケット([])で要素にアクセスしたり、オブジェクトをforeachで回したり、要素数をcountで数えることができるようになります。
実際は、$object['key']と書くと内部では$object->offsetGet($key)というメソッドが呼ばれるようになります。
そうです。これを巧く利用することで先ほどのような自動エスケープ処理を実装することが可能になります。

secure_arrayメソッドの実態

実際に拡張するために用意したコードは以下のようなシンプルなコードです。

<?php
function secure_array($v=array()) {
  return new EscapeArray($v);
}

Class EscapeArray extends ArrayObject {
  public function offsetGet($id)
  {
    $v = parent::offsetGet($id);
    if (is_array($v)) {
      array_walk_recursive($v, array($this, 'escape'));
    } else {
      $this->escape($v);
    }
    return $v;
  }
  public function escape(&$v)
  {
    $v = htmlspecialchars_decode($v, ENT_QUOTES);
    $v = htmlspecialchars($v, ENT_QUOTES, 'utf-8');
  }
}

array()のような呼び出しにより近づけるために、あえてインスタンス作成のためだけのsecure_array関数を用意しました。
そして、EscapeArrayというArrayObjectを拡張したクラスを作成し、配列にアクセスされるときに呼び出されるoffsetGetメソッドをオーバーライドし、ここでエスケープ処理されるようにしています。
あとは、配列の場合はその全ての値をエスケープするようにしていることと、2重エスケープされないようにエスケープ前にデコードしているぐらいです。
とてもシンプルですが強力です。

エスケープされていない値を取得する

常にエスケープさせることで安全になりましたが、エスケープさせていない値が欲しい場合もあるでしょう。
さきほどのサンプルの$tpl_valuesはEscapeArrayオブジェクトなので、メソッドを新しく持たせることができます。
これを利用して、エスケープさせない値を取得するgetRawメソッドを作ってみます。

<?php
class EscapeArray extends ArrayObject {
  ....
  public function getRaw($id)
  {
    return parent::offsetGet($id);
  }
}

エスケープさせないということは本来の値をそのまま返せばよいだけなので、ArrayObjectに用意されたoffsetGetを呼べばよいだけですね。
そうすると、サンプルコードは以下のように更に便利になります。

<?php
$str =  '<script>alert("test")</script>';

$tpl_values = secure_array();
$tpl_values['attack'] = $str;
echo $tpl_values['attack'];         // &lt;script&gt;alert(&quot;test&quot;)&lt;/script&gt;
echo $tpl_values->getRaw('attack'); // <script>alert("test")</script>

普通に配列として使っている限りはエスケープされ、エスケープしていない値が欲しいときはgetRawメソッドを使うというルールです。
まるでPHPの配列の仕様を拡張したかのようなことができてしまっています。

このサンプルコードを見て、「symfonyエスケープと使い方が似ている」と思った方正解です。これはsymfonyエスケープ実装方法もこれとほぼ同じでSPLを利用した拡張になっています*1。また、PHP5がPHP4に比べてどのように便利かというのが分かる例ですね。

これで、PHPXSSとは無縁さ!(いや。無理だろうな。。)

*1:ただし、ArrayObjectからの拡張ではなくIterator, ArrayAccess, Countableインターフェースの実装になっています