読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

output_add_rewrite_var()でCSRF対策してみる

output_add_rewrite_var() は、session.use_trans_sid で利用されているURLリライト機能に新しい名前と値のペアを追加する関数。

session.use_trans_sidの場合と同様、有効になるHTML要素と属性は url_rewriter.tags の設定によって決まります。

url_rewriter.tagsの設定は、デフォルトで "a=href,area=href,frame=src,input=src,form=fakeentry,fieldset=" となってますが、それぞれどのように書き換えられるか調査しました。

結論を書くと、書き換えられる要素が何であろうと、以下の仕様となっているようです。(PHP5.4.0 RC6で確認)

  • name はURLエンコード/HTMLエスケープ共にされない
  • value はurlencode()されるが、HTMLエスケープされない

いずれもHTMLエスケープされないばかりか、なぜか(というか「URLリライト」なので仕様通りと捉えるべきかもしれませんが)、formやfieldsetを指定した際に挿入される隠しフォーム()のvalue属性値までもが、パーセントエンコードされてしまいます。

名前および値は、URL (GET パラメータとして) およびフォーム (hidden フィールドとして) で追加されます。
これは、session.use_trans_sid で透過的 URL リライティングが有効になっている場合に セッション ID が渡される方法と同じです。

このように、PHPマニュアルのoutput_add_rewrite_var()関数の説明にある通り?で、フォームの場合でもエスケープ仕様は同様になってます。
検証コード

output_add_rewrite_var('" /><script>alert("Hello!");</script><input type="hidden" name="', 'foo');

なんて書くと、Hello!されてしまうわけです。
外部からの入力値を渡すような使い方はまずないとは思いますが、name,value共にエスケープ不要な文字列に限定した方が無難ですね。

この機能のために使わないタグを検出させても無駄なので、要件に合わせて調整します。

  • a=href,area=href,frame=src…フォーム送信時のトランザクショントークン用途なら不要。
  • input=src…type="image"の場合を想定しているのか分かりませんが、いずれにせよ意味が分からない。たぶん不要
  • form=fakeentry…PHPマニュアルにも説明がないけど、fakeentryを消してform=としても同様にform要素の直下に隠しフォームが出力される。
  • fieldset=form=fakeentryと同様で、fieldset要素の直下に隠しフォームが出力される。

fieldset要素については

HTML/XHTML strict に適合させたい場合には form エントリは削除し、 formフィールドの前後に<fieldset> タグを使ってください。

とありますが、form要素だってブロック要素だし別にform直下にinput要素置いてもvalidだと思うんですが、どういうわけでしょうか。

ともかく、今回の用途では ini_set('url_rewriter.tags', 'form='); で十分と判断。PHP_INI_ALLなんで神経質になる必要もないかと。

ちなみに

注意: もし出力バッファリングが有効になっていない場合、この関数を コールすると出力バッファリングが暗黙的に開始されます。

とある通り、output_buffering=Off の環境でob_list_handlers()を呼んでみたところ、"Array([0] => URL-Rewriter)" となりました。


ところで、なんでこんな関数を調べてたかというと、Hamachiya2さんの記事で紹介されていた http://ss.hamachiya.com/easy_csrf/ にインスパイアを受けて、汎用的なCSRF対策用のトランザクショントークン処理を書いてたからなのでした。