はじめに
この記事はnginxの設定ミスで起こるHostヘッダフォージェリの対策方法について検証した時のメモです。
$http_host
と$host
の両方でHostヘッダフォージェリが可能かどうかを調べます。
もし間違えている箇所がありましたら、指摘していただければ幸いです。
$http_host
リクエストラインに絶対URIを記述した場合
絶対URIに別のホストを指定すると、リクエストラインで指定されたホストにアクセスしますが、Hostヘッダはそのまま送信されます。
つまり、HOSTヘッダの値を改竄することでHostヘッダフォージェリが可能です。
リクエストラインに存在しないホストを指定した場合
nginxは、認識できないホスト名を持つリクエストを受信した場合
- listenディレクティブにdefault_serverが設定されているサーバー
- default_serverが設定されていない場合は、nginxが最初に読み込んだ設定のサーバー
にリクエストが送信されます。
例えば
#aaa.conf
server {
server_name aaa.example.com;
listen 80;
}
location / {
proxy_set_header Host $http_host;
proxy_pass http://192.168.11.10/echo.php?a;
}
と
# bbb.conf
server {
server_name bbb.example.com;
listen 80;
}
location / {
proxy_set_header Host $http_host;
proxy_pass http://192.168.11.10/echo.php?b;
}
のように、設定されていたとします。
この時、以下のようなリクエストを送信します。
GET https://hoge.example.com/ HTTP/1.1
Host: evil.com
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,ja;q=0.8
アプリケーションサーバーには以下のようなアクセスが来ます
xxx.xxx.xxx.xxx - - [14/Jul/2018:23:28:49 +0900] "GET /echo.php?a HTTP/1.0" 200 34 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36"
この場合ではdefault_serverを設定していない為、最初に読み込まれた設定のaaa.confのホストにリクエストが送信されています。
この時の $_SERVER["HTTP_HOST"]
の値と $_SERVER["SERVER_NAME"]
の値は以下の通りです
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 14 Jul 2018 14:49:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 34
Connection: close
X-Powered-By: PHP/7.0.30
Strict-Transport-Security: max-age=15768000
host:evil.com
server name:evil.com
Hostヘッダフォージェリに成功していることがわかります。
$host
$host
は以下の優先順位でホストが決定します。
- リクエストライン
- Host リクエストヘッダの値
- リクエストに一致するサーバー名
リクエストラインに絶対URIを記述した場合
以下のようなリクエストを送信します
GET https://example.com/ HTTP/1.1
Host: evil.com
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,ja;q=0.8
$hostはHostヘッダよりもリクエストラインを優先するので、アプリケーションに送信されるHostヘッダの値は example.com
になります。
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 14 Jul 2018 15:43:57 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 54
Connection: close
X-Powered-By: PHP/7.0.30
Strict-Transport-Security: max-age=15768000
host:example.com
server name:example.com
しかし、存在しないホストをリクエストラインに記述したり、Hostヘッダを改竄することで同様に
- listenディレクティブにdefault_serverが設定されているサーバー
- default_serverが設定されていない場合は、nginxが最初に読み込んだ設定のサーバー
にリクエストを送信してしまいます。
- リクエストラインに存在しないホストを記述した場合
GET https://evil.com/ HTTP/1.1
Host: example.com
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,ja;q=0.8
レスポンス
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 14 Jul 2018 16:16:30 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 34
Connection: close
X-Powered-By: PHP/7.0.30
Strict-Transport-Security: max-age=15768000
host:evil.com
server name:evil.com
- Hostヘッダを改竄した場合
GET /safe HTTP/1.1
Host: evil.com
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,ja;q=0.8
レスポンス
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 14 Jul 2018 16:20:11 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 34
Connection: close
X-Powered-By: PHP/7.0.30
Strict-Transport-Security: max-age=15768000
host:evil.com
server name:evil.com
## わかったこと
$http_host
の代わりに$host
を使用した場合でも
存在しないホストをリクエストラインに記述したり、Hostヘッダを改竄することで
- listenディレクティブにdefault_serverが設定されているサーバー
- default_serverが設定されていない場合は、nginxが最初に読み込んだ設定のサーバー
に偽装したホスト名を送信することが可能ということがわかりました。
対策
サーバーセキュリティ
それでも対策は簡単です。
-
$http_host
の代わりに$host
を使用してください。 - 認識できないホスト名を持つ全てのリクエストをキャッチするための専用の仮想ホストを作成してください。
アプリケーションセキュリティ
管理者がどのホスト名を信頼しているかを完全に自動的に特定する方法はないため、この問題を解決するのは難しいです。最も安全な解決策ではありますが、アプリケーションの初期設定時に信頼できるドメインのホワイトリストを設定するというアプローチを使用することです。
PHPでは $_SERVER["SERVER_NAME"]
について以下のように記述されています。
'SERVER_NAME'
現在のスクリプトが実行されているサーバーのホスト名です。 スクリプトがバーチャルホスト上で実行されている場合は そのバーチャルホスト名となります。注意: Apache 2 では、UseCanonicalName = On と ServerName を設定する必要があります。 そうしなければ、この値はクライアントが提供するホスト名を指すようになってしまい、無意味です。 また、セキュリティ的な意味合いでこの値に頼ってはいけません。
nginxの場合は
fastcgi_param SERVER_NAME "example.com";
のようにして、SERVER_NAMEを指定することで利用できますが、これは環境依存になるのでオススメできません。まだ定数として保持してそれを利用した方が良いと考えます。
参考記事
対策方法は以下のリンクの一部抄訳です。
https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html