sfFormとエスケープに関する注意点

symfonyで自動エスケープ設定を有効にしている場合、アクションからテンプレートに変数をセットする際にオブジェクトならばsfOutputEscaperクラスでラッピングされ、スカラーであれば直接エスケープされてテンプレートに渡されます。

スカラー値をテンプレートに渡す際にエスケープしない方法 - ゆっくり*ゆっくり

上の記事を読んでいる前提で。

symfonyでフォームを扱う場合はsfFormクラスを利用しますが、自動エスケープが有効になっていても、sfFormはエスケープ対象になりません。なぜならば、sfFormはHTMLの出力を行うクラスなので、これをエスケープ対象にしてしまうと機能が成り立たないからです。

これは上の記事で説明したsfOutputEscaper::markClassesAsSafe()メソッドを利用しています。sfView::initialize()の内部で、次のコードが必ず呼ばれます。

<?php

class sfView
{
  public function initialize($context, $moduleName, $actionName, $viewName)
  {
    // ...

    sfOutputEscaper::markClassesAsSafe(array('sfForm', 'sfFormField', 'sfFormFieldSchema', 'sfModelGeneratorHelper'));

    // ...
  }
}

さて、たとえばsfFormDoctrineを継承したUserFormがあるとします。

<?php

class fooActions extends sfActions
{
  public function executeBar(sfWebRequest $request)
  {
    $user = new User();
    $user->setName("<script>alert('ゆどうふ')</script>");
    $this->form = new UserForm($user);
  }
}

さてさて、これをテンプレートで次のようにすると...

<?php echo $form->getObject()->getName() ?>

動かせばわかるんですけど、JavaScriptが実行しちゃいます。仕組みを知っていれば当然のことなのですが、sfOutputEscaperでラッピングされていないオブジェクト($form)の内部に持つオブジェクトを取得($form->getObject())しようとした際、それがエスケープされることはありません。その他にも、sfForm::getValue()の値を出力しようとした場合も当然エスケープされません。

ちなみにフォームがエスケープ対象から外れるといって、render()メソッドを用いて何かHTMLのレンダリングをしようとした際、一切エスケープが行われないわけではありません。sfWidget::escapeOnce()というメソッドがあり、これを用いて必要に応じてエスケープを行います。

ちなみにこのescapeOnce()もまた曲者で、このメソッドに '&lt;' って文字列を渡すと、エスケープしたら '&amp;lt;' になってほしいのに、'&lt;' のままになってしまいます。まあ詳細は中を読んでいただければ。。。

というわけで何が言いたいかというと、sfFormの値をテンプレートで直接出力するのはXSSにつながる危険性があるので気を付けてくださいねってことです。解決策は単純で、sfFormの内部に持つ値をアクション側で変数として受け取り、それをビューに渡してあげるだけです。つまり先ほどの例でいえば、

<?php

class fooActions extends sfActions
{
  public function executeBar(sfWebRequest $request)
  {
    $user = new User();
    $user->setName("<script>alert('ゆどうふ')</script>");
    $this->form = new UserForm($user);
    $this->user = $form->getObject();   // アクションから個別にテンプレートに渡す
  }
}
<?php echo $user->getName() ?>

こうすればちゃんとエスケープされます。