概要
Apache HTTP Server(以下Apache)だけで、あるURLパスにアクセスされた際に、付与されたパラメータで指定されたURLへリダイレクトする必要があり、試行錯誤したときの話です。
環境
- CentOS7
- Apache Httpd Server 2.4.6
要件
- URLパス
/redirect
へアクセスされたときに、パラメータredirect
で指定されたURLへリダイレクトする
http://example.com/redirect?redirect=http://other.example.com
↓ リダイレクト
http://other.example.com
試行錯誤
バックエンドへプロキシせずにApacheの処理でリダイレクトを行う必要があったため、Rewrite設定で対応することにしました。
※ 設定はRewriteの部分のみに、パラメータの値取得もredirectのみの想定と簡略化しています
※ 正規表現は厳密性よりも可読性を重視しています
試行
単純にパラメータを抽出して、条件に合っている場合にリダイレクトすればOK!と考えて以下のような設定にしました。
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値を設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:%1]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
無事にhttp://other.example.com
へリダイレクトしました。よし!終了。
錯誤
パラメータってURLエンコードされるよね。以下のような場合は大丈夫?
http://example.com/redirect?redirect=http%3A%2F%2Fother%2Eexample%2Ecom
試してみる。むむっ
http://example.com/http%253A%252F%252Fother%252Eexample%252Ecom
にリダイレクトされてしまいました。
そのままRewriteしたところ、パスとして扱われる結果に。。。
二重にエンコードされているので、RewriteフラグNE
をつけてみることにします。
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値を設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:%1]
RewriteCond %{ENV:RED_PARAM ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD,NE]
http://example.com/http%3A%2F%2Fother%2Eexample%2Ecom
二重エンコードは解消したが、やはりだめでした。
mod_rewriteの動作
mod_rewriteの説明では、RewriteRuleの置換文字列には、システムファイルパス・URLパス・絶対URL・ダッシュ(-)を指定することができると記載されています。
mod_rewriteのソースを確認したところ、絶対URLと見なされるためには、置換文字列中に://
が含まれていることが条件で、上記の要件の場合、パラメータがURLエンコードされているときはデコードする必要がありました。
そこで引数の文字列をURLデコードして標準出力するシェルスクリプト(decode.sh
)を作成、何とか呼び出せないかと調査を開始しました。
RewriteMapディレクティブを発見
ReweriteMapで外部プログラムを呼び出すことができるとの情報を見つけたので、さっそくデコードするシェルスクリプトを呼び出してみます。
プログラムは、標準入力を待ち受けて、標準出力へ改行で終わる文字列を出力するものでなければいけないため、引数から標準入力を待ち受けるようシェルスクリプトを修正しました。
RewriteMapでマップ名decode
に、プログラム呼び出しprg
を書いてコロン区切りでプログラムファイルconf/decode.sh
を指定する以下の設定にしました。
# decodeというマップ名に外部プログラムconf/decode.shを設定
RewriteMap decode prg:conf/decode.sh
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値をデコードして設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:${decode:%1}]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
ちゃんとhttp://other.example.com
へリダイレクトすることができました!
しかし、よくよくRewriteMapについて調べると外部プログラム以外にもマッピング種別があり、escape(エンコード)・unescape(デコード)・uppercase・lowercaseを内部ファンクションとして呼び出せることが判明!
試しにRewriteMapの設定を変更して、内部ファンクションのunescapeを指定してみます。
# decodeというマップ名に内部ファンクションunescapeを設定
RewriteMap decode int:unescape
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値をデコードして設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:${decode:%1}]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
外部プログラムをRewriteMapで呼び出したときと同様、期待通りに動作しました!
今回お世話になったRewriteMapについて調べたことを紹介します。
RewriteMapディレクティブの書き方
RewriteMap MapName MapType:MapSource
-
MapName
マップを呼び出すときの名称
呼び出すときは、${MapName:LookupKey\|DefaultValue}
で指定することが可能です。
LookupKeyはマップのキー。
DefaultValueは省略可能。 -
MapType
とMapSource
MapType | MapSource | 説明 |
---|---|---|
txt | プレーンテキスト形式のマッピングファイルのパス | KeyとValueをスペース区切りで1行に記述する形式。 |
rnd | プレーンテキスト形式のマッピングファイルのパス | KeyとValueをスペース区切りで1行に記述する形式。Valueは複数の値をパイプで区切った形式。ランダムでいずれかの値が選択される。 |
dbm(=DBType) | Key-Value形式のDBMファイルのパス | DBTypeはsdbm, gdbm, ndbm, dbで作成したファイルが使用可能だが、Apache HTTP Serverのユーティリティhttxt2dbmでテキストファイルから作成したdbmファイルが推奨されている。 |
int | toupper,tolower,escape,unescape | MapSourceのいずれかの内部ファンクションを指定する。 toupper:大文字にする tolower:小文字にする escape:URLエンコードする unescape:URLデコードする |
prg | 実行するプログラムファイルのパス | プログラムはSTDINから一つの引数(Key)をとり、STDOUTで一つの改行で終了する文字列(Value)を出力する必要がある。 |
dbd,fastdbd | クエリ | 一つの引数(Key)を与え、一つの値(Value)を返すクエリを記述する。mod_dbdでDBアクセスが行えるよう設定してあることが前提。fastdbdは、クエリキャッシュを作成するため高速。 |
RewriteMapディレクティブの使いどころ
RewriteMapを使用することで、長くなりがちなRewrite設定を簡略化することが可能です。
RewriteRule /foo http://foo.example.com [L]
RewriteRule /bar http://bar.example.com [L]
RewriteRule /hoge /example/hoge [L]
...
以下のように置換定義を別のファイルでシンプルに定義することが可能です。
RewriteRuleが多数定義されている場合、置換定義を別のファイルで定義してApacheの設定ファイルを簡略化できるか検討したほうがよさそうです。
RewriteMap mapname txt:conf/rewritemap.txt
RewriteRule /(.+) ${mapname:$1} [L]
foo http://foo.example.com
bar http://bar.example.com
hoge /example/hoge
また、置換定義が非常に多い場合は、txtの代わりにdbmファイルを使用することを検討しましょう。
これは、LookupKeyにインデックスが作成されるためです。
Apacheに付属されているhttxt2dbm
コマンドを使用してdbmファイルを作成することができます。
httxt2dbm -i rewritemap.txt -o rewritemap.dbm
コマンドを実行すると、rewtitemap.dbm.dir
とrewritemap.dbm.pag
ファイルが作成されます。
dbmファイルを利用するときのRewriteMapの設定は以下のようになります。
RewriteMap mapname dbm:conf/rewritemap.dbm
RewriteRule /(.+) ${mapname:$1} [L]
他にもMapType
がありますが、今回はここまでとします。
注意
- パラメータURLへのリダイレクトでは、予期せぬリダイレクト先にリダイレクトされることが考えられるため、パラメータのバリデーションは行いましょう。バリデーションを行わない場合、脆弱性となる恐れがあります。記事中の設定では省略されていますが、正規表現を用いたRewriteCondなどでリダイレクト先URLのバリデーションを行い、NGの場合はエラー画面にリダイレクトするなど考慮しましょう。
https://qiita.com/tamura__246/items/f41d2f69f1c8494493e0 - RewriteMapの外部プログラムマッピングでは、プログラムが終了するまで待機します。応答時間の遅延の原因となる可能性がありますので注意しましょう。
- URL中に改行コードが含まれている場合は、削除する・エラーとするなど対策を行いましょう。
https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_7.html - その他、ディレクトリトラバーサル対策など脆弱性対策を確認・実施しましょう。
最後に
初めからApache HTTP Serverの調査をしっかり行うべきでした。。。
Apache HTTP Serverのモジュールはたくさんあるため、もっと良い方法があるかもしれません。
もっと良い方法があるよ、という方は教えていただけるとうれしいです。