Hostヘッダフォージェリの対策について

# はじめに
この記事は[nginxの設定ミスで起こるHostヘッダフォージェリ](https://qiita.com/no1zy_sec/items/dfccd91cf76bf73b7754)の対策方法について検証した時のメモです。

`$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` は以下の優先順位でホストが決定します。

1. リクエストライン
2. Host リクエストヘッダの値
3. リクエストに一致するサーバー名

## リクエストラインに絶対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`を使用してください。
* 認識できないホスト名を持つ全てのリクエストをキャッチするための専用の仮想ホストを作成してください。

https://www.nginx.com/resources/wiki/start/topics/examples/server_blocks/

### アプリケーションセキュリティ
管理者がどのホスト名を信頼しているかを完全に自動的に特定する方法はないため、この問題を解決するのは難しいです。最も安全な解決策ではありますが、アプリケーションの初期設定時に信頼できるドメインのホワイトリストを設定するというアプローチを使用することです。

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