Apache 2.4.49でのpre_translate_nameフックの追加
少し前に影響の大きい脆弱性(CVE-2021-41773)が見つかったApache 2.4.49ですが、このバージョンからフックが追加されたようです。
元々、translate_name というフックがあったのですが、さらに pre_translate_name というフックが追加されています。
translate_name フックはリクエストURLの変換を行うためのフックで、Alias(mod_alias) の処理などで使われています。ファイルシステム上のファイルへのリクエストの場合、最終的にURLをファイルパスへ変換しているのもこのフックです(*1)。
translate_name フックはURLがURLデコード(URL中の%xxを元に戻した形式)した後に呼び出されるのですが、pre_translaet_name フックはURLデコード前のタイミングでURL変換を行えるように呼び出されます。以下は ap_process_request_internal() 内での各フックの実行タイミングのイメージです。
フック呼び出しタイミングのイメージ
AP_DECLARE(int) ap_process_request_internal(request_rec *r) { /* * ブラウザから /page/alice and bob のようなURLにアクセスすると * スペースは%20にエンコードされてリクエストされる。 * r->uri: /page/alice%20and%20bob * r->uriにはリクエストのパス部分が設定されている */ /* 英数字と-_.~ 以外の文字(RFC3986参照)のURLデコードや * /../の除去等を行う。 * この時点では英数字と-_.~以外の文字はURLデコードされない。 */ ap_normalize_path(r->parsed_uri.path, ...); /* pre_translate_nameフック実行(URLデコード前) */ ap_run_pre_translate_name(r); /* URLデコード実行。r->uri:/page/alice and bob */ ap_unescape_url(r->parsed_uri.path); /* translate_nameフック実行UR(URLデコード後) */ ap_run_translate_name(r);
なぜ、pre_translate_nameフックが追加されたのか?
2.4.48と2.4.49のソース差分を見る限りではmod_proxyのために追加されたようです。
また、以下のページには
https://www.mail-archive.com/dev@httpd.apache.org/msg74128.html
”We need a way to forward non %-decoded URLs upto mod_proxy (reverse)"のような記述もあります。
上記のスレッドを追う限りでは、Java ServletにProxyする際に、mod_proxyでもmod_jkと同じルールでURL正規化した上でURLのマッチングを行いたかったのが始まりのようです。Servlet用にリクエストURLからパスパラメータ(*2)を除去する処理などが加えられています。
その過程で、
以下のURLは本来異なるものである。
(1) /docs/..%3Bfoo=bar/%3Bfoo=bar/test/index.jsp
(2) /docs/..;foo=bar/;foo=bar/test/index.jsp
ProxyPass /docs ajp://localhost:8009/docs
このProxyPass設定があった場合、
(1)は ProxyPassにマッチする。
(2)はServlet用のURL正規化によりパスパラメータを除去後(/docs/..//test/index.jsp)、親ディレクトリ指定(..)や二重スラッシュを除去して、最終的にリクエストURLは /test/index.jsp になるのでProxyPassにはマッチしない。
という動作になるべきだが、現状(〜2.4.48)ではURLデコード後にURLのマッチングをすることしかできないので、(1)も(2)と同じURLとなってしまいProxyPassにマッチしない。これを正しく処理するには、URLデコード前のURLをmod_proxyで評価する手段が必要である。
という議論があり、pre_translate_name フックが必要になったようです。
mod_proxyにおいてpre_translate_nameはどのように使われているか
ProxyPassの設定があると、translate_name フックに登録されている mod_proxy.c の proxy_trans() でリクエストURLのマッチングが行われ、必要に応じてURLの書き換えが行われます。
2.4.48までは translate_name フックで処理するしかなかったのですが、2.4.49からProxyPassのオプションに mapping=(encoded|servlet) というものが追加されています(*3)。ProxyPass に mapping オプションを指定しておくとそのエントリは pre_translate_name フックで処理されるようになります(URLデコード前に処理される)。
mapping オプションの有無で処理するフックを決めるというわけです。
# translate_name フックで処理される(今までどおり)。 ProxyPass /foo http://backend-server # mapping=encoded/servlet があると pre_translate_name フックで処理される。 # translate_name では処理されない。 ProxyPass /bar http://backend-server mapping=encoded

mapping=encodedの例
実際にどのように使うか見ていきます。
/backend%20fooにマッチするURLは別サーバーにProxy(Reverse Proxy)したいとします。(*4)
http://target-server/backend%20foo/something のようなアクセスが正しくReverse Proxyされるには、以下のようにURLデコードされたパスでProxyPassを記述する必要があります。
# パスはURLデコードされたもので記述する必要がある
ProxyPass "/backend foo" http://backend-server
以下のようにURLエンコードを含むパスではマッチしません。
ProxyPass /backend%20foo http://backend-server
この場合、2.4.49から追加された mapping=encoded を指定すれば pre_translate_name フックでURLデコード前にマッチング処理が行われるのでProxyPassにマッチするようになります。
ProxyPass /backend%20foo http://backend-server mapping=encoded
このようにmapping=encodedを使えばURLエンコードされた文字をマッチング部分に使うことができるようになります。
mapping=servletの例
mapping=servletの指定がある場合は、pre_translate_name で処理されるのは mapping=encoded と変わりませんが、Servlet用にURL正規化が行われた後にマッチングが行われます。具体的にはパスパラメータ(例: ;foo=bar/)の削除処理が行われています(*5)。
上記のメーリングリストにもあった以下のURLで動作を見ていきましょう。
(1) /docs/..%3Bfoo=bar/%3Bfoo=bar/test/index.jsp
(2) /docs/..;foo=bar/;foo=bar/test/index.jsp
ProxyPass /docs http://backend-server
この設定では(1),(2)どちらもマッチします。本来マッチして欲しくない(2)もマッチします。
[注釈]
translate_name フックで処理されるので、(1)はURLデコードされて(2)と同じURLになってからマッチングされる。
Servlet用のURL正規化は行われずそのままマッチングされるのでProxyPassにマッチする。
ProxyPass /docs http://backend-server mapping=servlet
mapping=servletを指定すると、(1)はマッチ、(2)はマッチせず期待した動作になります。
[注釈]
pre_translate_name フックで処理されるので、(1)は /docs/..%3Bfoo=bar/%3Bfoo=bar/test/index.jsp のままマッチングされマッチする。
(2)はServlet用のURL正規化でパスパラメータが削除され /docs/..//test/index.jsp に変換される。その後、親ディレクトリ指定(..)や二重スラッシュが除去され、リクエストURLは/test/index.jspになるのでマッチしない。
おまけでmapping=encoded指定をした場合を見てみます。
ProxyPass /docs http://backend-server mapping=encoded
この場合は、(1)は /docs/..%3Bfoo=bar/%3Bfoo=bar/test/index.jsp のままマッチングされるのでマッチします。
(2)も mapping=encoded ではServlet用のURL正規化が行われないので、パスパラメータが除去されずURLは /docs/..;foo=bar/;foo=bar/test/index.jsp のままマッチングされます。このためマッチします。
これで、なぜ pre_translate_name フックが追加されたのか、mappingオプションがどのような動作をするのか理解できたと思います。
ついでに脆弱性(CVE-2021-41773)について
これまで説明したように主にServletへProxyする時のURLマッチングを改善するために、pre_translate_nameフックとmappingオプションが追加されました。
その過程で2.4.48までは、ap_getparent()("/../"の削除)やap_no2slash()(二重スラッシュの除去)で行われていた各処理がap_normalize_path()に置き換えられました。 この関数の中で英数字のURLデコード処理、親ディレクトリ指定(..)の除去、二重スラッシュなどをまとめて行うようになったためバグが入り込んでしまったようです。
具体的には、URLデコードしながら"/../"の存在チェックをしているので、デコード中の"/.%2e/"を"/../"と比較してしまい、一致せず処理をすり抜けています。
ap_normalize_path()のかなり大雑把な動作イメージ
while Path: "/%2e%2e/" 1文字分URLデコード Path: "/.%2e/" 親ディレクトリ指定か判定 ".."かチェックするが次の文字はまだエンコードされたまま(%2e)なのでチェックをすり抜ける if Path == ".." "/../"削除処理
URL周りの処理はエンコードや正規化を意識しないといけないので地味に大変ですね。
参考ページ
メーリングリストによる議論
https://www.mail-archive.com/dev@httpd.apache.org/msg74128.html
ap_normalize_path()が導入されたcommit
https://github.com/apache/httpd/commit/4c79fd280dfa3eede5a6f3baebc7ef2e55b3eb6a
Request Processing in the Apache HTTP Server 2.x
https://httpd.apache.org/docs/2.4/developer/request.html
(*1) coreモジュールのap_core_translate()関数がtranslate_nameフックの末尾に登録されています。
(*2) パス中に埋め込まれているセミコロンで始まるname/valueの組み合わせ(例: ;foo=bar/)
(*3) なぜかドキュメントに記載はないので、まだ実験的なものなのかもしれません。
(*4) あまりいい例が思いつかなかったので不自然なURLですがご容赦を
(*5) マッチング用のURL文字列から削除されるだけで、転送先サーバーへのリクエストURLからは削除されません。
投稿日:2021/10/13 14:41
タグ: Server