URLのパス部分に%2Fを含む時の扱い
お知り合いのページを見て初めて知ったのですが、Apacheは標準ではパス部分に %2F(/をエスケープしたもの) を含むURLに対して404を返すそうです。
このままだと、最近流行り(?)の "/" でパラメータを区切る場合に、"/" 自体を値に含めることができません。そこでApacheの設定で、AllowEncodedSlashes を On にすると、URL中に %2F も使用できるようになります。
というのがメインな部分なのですが、その中で、
ところで、RFCとか規則上って、「/」をURL encodeして「%2F」にしても良いのか?
AllowEncodedSlashes ディレクティブにはまる。
それともしてはいけないのか。これがよく分からん。
という疑問が書かれていたので、RFCでどうなっているのかちょっとだけ調べてみました。
3.2.3 URI Comparison
Characters other than those in the "reserved" and "unsafe" sets (see
RFC2616 Hypertext Transfer Protocol -- HTTP/1.1
RFC 2396 [42]) are equivalent to their ""%" HEX HEX" encoding.
URIの "reserved" と "unsafe" 以外は、エスケープされたものと等しく扱うみたいです。
そしてRFCでのURIの定義はこんな感じです。
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
RFC2396 Uniform Resource Identifiers (URI): Generic Syntax
これを見ると "/" は "reserved" に含まれていますね*1。これらを単純に解釈すると、"/" と %2F は等しくは扱われないのだと思います。
RFC2616の先ほどの行の後に具体的な例が書かれています。
For example, the following three URIs are equivalent:
http://abc.com:80/~smith/home.html
RFC2616 Hypertext Transfer Protocol -- HTTP/1.1
http://ABC.com/%7Esmith/home.html
http://ABC.com:/%7esmith/home.html
これは ~ が "reserved" に含まれていないので、等しく扱うということだと思います。
一方、"/" は "reserved" に含まれているので、以下の二つは等しくないはずです。
http://abc.com/hoge/hoge.html http://abc.com/hoge%2Fhoge.html
ざっと見た限りでは、これくらいしか記述がありませんでした。これらの部分は、等しいかどうかだけですので、サーバの振る舞いまでは制約を受けないのかもしれませんが、Apacheが "/" と "%2F" を同じに扱うのはRFC的に微妙な気もしますね。
PATH_INFOの仕様
さて、これでApache自身がファイルを扱う場合は分かったのですが、もう1点気になった部分があります。
PATH_INFOが/で渡されたか、%2fで渡されたか判断がつかなくなる。まぁ、PATH_INFOは変な値を持ってることが多いので(つか、勝手に正規化されていたりする)使わないようにしているので、問題なし(ぉぃ
AllowEncodedSlashes ディレクティブにはまる。
なんでそんな不便な動作なんだろう? と思って AllowEncodedSlashes のドキュメントを見てみると、
符号化されたスラッシュを許可することは、復号をすることを 意味しません。%2F や (関係するシステムでの) %5C は、他の部分が復号された URL の中でもそのままの形式で 残されます。
http://httpd.apache.org/docs/2.0/ja/mod/core.html#allowencodedslashes
あれ、%2F はそのまま残るって書かれていますね。どういうことかと調べてみると、該当するバグが bugzillaに登録されてました。
%2F will be decoded in PATH_INFO (Documentation to AllowEncodedSlashes says no decoding will be done)
https://issues.apache.org/bugzilla/show_bug.cgi?id=35256
本来は %2F を残すために ap_unescape_url_keep2f() という関数でデコードしてるんだけど、実際は %2F を含めて全部デコードしちゃうらしいです。(関数名に keep2f って付いてるのに…)
2.0.47では大丈夫だったけど、2.0.52ではダメだったと書かれています。何かのタイミングで関数を書き換えちゃったのでしょうか?
ちなみに、
this bug exists in 2.2.6 as well
https://issues.apache.org/bugzilla/show_bug.cgi?id=35256
とあるので、まだ治ってないみたいです。とありますが、追記2に書いたようにtrunkでは修正されてます。もっとも、今更変更しても混乱するだけなので、%2Fもデコードしてしまうのが実質的な仕様になるのでしょうね。
というわけで結論としては、やっぱりPATH_INFOで "/" と "%2F" を区別する方法はないみたいです。
実際Googleしてみると、PATH_INFO は怪しいので REQUEST_URI を使いましょう、という記事がいっぱい出てきます。みんな PATH_INFO は使っていないので、バグも気にならないのかもしれませんね。
(追記)PATH_INFOが壊れたのはいつ?
2.0.47では大丈夫だったけど、2.0.52ではダメだったと書かれています。何かのタイミングで関数を書き換えちゃったのでしょうか?
と書いた後に少し気になったので、実際にソースコードの変更履歴を調べてみました。
まず現在のコードになった場所はここのようです。
handling of encoded non-slashes was borked in the
AllowEncodedSlashes paththanks to FirstBill for pointing that out!
http://svn.apache.org/viewvc/httpd/httpd/trunk/server/util.c?view=log#rev104937
どうやら既存コードだと壊れてしまうので、修正したってことみたいです。
そして肝心の修正内容はこんな感じです。
decoded = x2c(y + 1); - if (!IS_SLASH(decoded)) { - *x++ = *y++; - *x = *y; + if (decoded == '\0') { + badpath = 1; } else { *x = decoded; y += 2; - if (decoded == '\0') { - badpath = 1; - } }
元々はIS_SLASHでデコード結果が / かどうかを判定していたようですが、バグ修正の際に勢い余って消してしまったような感じですね。
さて、この修正の付近をよく見てみると、実は前日にも同じ場所が変更されていました。
Fix the handling of URIs containing %2F when AllowEncodedSlashes is enabled.
http://svn.apache.org/viewvc/httpd/httpd/trunk/server/util.c?view=log#rev104925
Previously, such urls would still be rejected with 404.
この修正前までは、AllowEncodedSlashesオプションを指定しても、実際には動作していなかったはずだ、と書かれています。そして実際の修正内容はこんな感じです。
char decoded; decoded = x2c(y + 1); - if (IS_SLASH(decoded)) { + if (!IS_SLASH(decoded)) { *x++ = *y++; *x = *y; }
IS_SLASHの条件判定が逆になっています。
ところがこの部分は、デコード結果が / だった場合に、デコード前の文字(%2F)の方を採用するルーチンのようです。そのため、この条件判定を逆にしてしまうと、全部の文字がデコードされなくなります。
ということで結論としては、この時の修正に失敗したものを、翌日に再修正してみたら別のバグが混入した、という悲しい理由だったみたいです。修正日は2004年の9月ですので、ずいぶんと昔から今の動作になっていたようです。
ちなみに元々のコードですが、デコード前の文字列が(%2Fのように)3文字あるはずなのに、2文字しかコピーしていません。本来は下記のように修正するべきだったのだと思います。
if (IS_SLASH(decoded)) {
*x++ = *y++;
+ *x++ = *y++;
*x = *y;
}
ただロジック的には、最初の % さえコピーしてしまえば、2文字でも3文字でも別に問題ないコードに見えます。わざわざ修正したということは、何か問題が発生する条件があったのでしょうかね?
ちなみにbugzillaに投稿されているパッチの一つは、上記のコードと同じ内容になっています。
(追記2)修正されてた
よく見たら2007年の9月に修正されてたみたいです。unescapeが一つの関数にまとまってスッキリした作りになっています。それとreservedな文字列を無視できるようになっていて、ちゃんとRFCに書かれた通りに動作可能になったみたいです*2。
時期的にはbugzillaで議論してる最中に既に直ってたみたいですが、その後にも症状が出るという人がいるところを見ると、ディストリビューションによっては含まれていなかったりするのかもしれませんね。