環境: 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を使った方が良いと思う。
そもそもURLのパス部分には特殊文字を含めてはいけないので、Cakeに限った問題ではないと思いますよ。mod_rewiteを使ってパラメータをパスにくっつけるタイプの仕組みには悩ましい問題かもしれませんね。
一応こちらを参考にしてみてください。
http://cakephp.jp/modules/newbb/viewtopic.php?topic_id=2344&forum=3&post_id=5943#forumpost5943
コメントありがとうございます。
調べてみると、少なくとも ‘/’ だけはApacheのデフォルト設定では
%エンコードしても扱えないようですね(URI仕様上は問題ないはず)。
AllowEncodedSlashesで検索すると他フレームワークの事例が
いろいろと見つかります。
http://httpd.apache.org/docs/2.0/ja/mod/core.html#allowencodedslashes
しかしそれ以外の特殊文字に関しては%エンコーディングで問題ないので、
フレームワークが適切にエスケープすべきではないかと思います
(SQLの中で ‘ を使う場合とかと同じことで)。
デコード前のURLは $_SERVER['REQUEST_URI'] 等から得られるはずですが、
Apache以外ではよくわからないので、
Cakeのチケットにでも投げて訊いてみるつもりでした。
[...] 検索パラメータの引き継ぎにnamedを使うのは問題が多い(参考)。代わりにQueryStringを使いたい。 [...]