環境: CakePHP 1.3
CakePHPはURLパラメータのエンコードを一切行わないため、特殊な文字がパラメータに入り込むと容易にルーティングが破綻する。ここで言う「URLパラメータ」とはRoute中に埋め込まれたパラメータ、namedパラメータ、passパラメータのことを指す。
例えば以下のようなRouteがあったとする。
/controller/action/:keyword
ここでパラメータkeywordに特殊な文字を与えると…
keyword => '%'
URL: /controller/action/%
結果: URLとして不正な形式なので"400 Bad Request"になる
keyword => '?foo'
URL: /controller/action/?foo
結果: ?以降はQueryStringと見なされてルーティングから除外され、Routeにマッチしなくなる
keyword => 'a/b/c'
URL: /controller/action/a/b/c
結果: /がそのままパラメータ区切りになるのでRouteにマッチしなくなる
この問題は単純に rawurlencode/rawurldecode を使うだけでは回避できない。またCakeの設計上の問題なので、簡単な修正方法もない。よって任意の文字が含まれる可能性のあるパラメータをURLに埋め込んではいけない。最も簡単な回避策は、URLパラメータを諦めてQueryStringを使うことである。
Router::url(array('?' => array('keyword' => $any_characters)));
問題の原因と回避策の考察
なぜ rawurlencode で回避できないかというと、CakePHPが mod_rewrite 経由で $_GET['url'] から元のURLを取得しているからだ。
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
RewriteRuleの後方参照($1の部分)では%エンコーディングのデコードが行われる。またQueryStringから $_GET に格納される際にもPHPによってデコードが行われる。つまりCakeのルーティングが実行される前に、既に2回のデコードが行われている。よってもしこの問題を rawurlencode で回避するなら、3重に rawurlencode した上で rawurldecode しなければならない、ということになる。
ちなみに RewriteRule の[B]オプションを指定することで後方参照のデコードを回避することができる。この場合は rawurlencode は2回で済む。しかしそれでもURLが汚くなることは変わりないし、検索エンジンのロボット等もURL中の文字列を認識してくれないだろう。
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L,B]
別の考え方として、%エンコーディング以外の安全なエンコーディングを用いる方法もある。例えば16進数のバイナリ表現に変換すれば、[a-f0-9]しか現れないので確実に安全である。
$encoded = bin2hex($data);
//[0-9a-f]しか含まれないことを事前にチェックしておくこと
$decoded = pack('H*', $encoded);
ただしこの方法だと長さは元の文字列の2倍になるし、可読性も全く損なわれる。ここまでするくらいならやはりQueryStringを使った方が良いと思う。