PHPにおけるHostヘッダインジェクション攻撃が可能な脆弱性
Googleで「Hostヘッダインジェクション」を検索しても「HTTPヘッダインジェクション」しか出てこないので、この記事を書くことにしました。
ちなみに、「Hostヘッダインジェクション」自体はPHPに固有というわけではなく、あらゆる環境で起こり得ます。
Hostヘッダインジェクションとは?
HTTPリクエストの「Hostヘッダ」の値を攻撃者が操作する攻撃です。
例えば、以下のようなコードがあった場合、$_SERVER['HTTP_HOST']
の値を操作できれば、リンク先を自由に変更できます。
<a href="http://<?php echo $_SERVER['HTTP_HOST']; ?>/?token=secret">
例えば、PHPビルトインWebサーバの場合ですが、telnetしてアクセスしてみましょう。
$ telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1 <-- ここからリクエストヘッダ
Host: evil.example.jp <-- ここで不正な値を注入
HTTP/1.1 200 OK <-- ここからレスポンスヘッダ
Host: evil.example.jp
Connection: close
X-Powered-By: PHP/5.5.9-1ubuntu4.14
Content-type: text/html
<a href="http://evil.example.jp/?token=secret">
Connection closed by foreign host.
HTTPリクエストでのHostヘッダを「Host: evil.example.jp
」と指定していますので、ホスト名は「evil.example.jp」になっており、その値がそのまま出力されることがわかります。
つまり、このような環境でサーバのURLをHostヘッダの値から生成している場合は、この脆弱性になります。
$_SERVER['HTTP_HOST']と$_SERVER['SERVER_NAME']の仕様
$_SERVER['HTTP_HOST']
がダメなら$_SERVER['SERVER_NAME']
を使えばいいじゃないと思うかも知れませんので、そちらも検証しておきます。
test.php
<?php
echo $_SERVER['HTTP_HOST']."\n", $_SERVER['SERVER_NAME']."\n";
上記のファイルをApacheのドキュメントルートに置きました。
$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /test.php HTTP/1.1
Host: evil.example.jp
HTTP/1.1 200 OK
Date: Fri, 06 Nov 2015 00:42:26 GMT
Server: Apache/2.4.12 (Unix) OpenSSL/1.0.1m PHP/5.5.24 mod_perl/2.0.8-dev Perl/v5.16.3
X-Powered-By: PHP/5.5.24
Content-Length: 31
Content-Type: text/html
evil.example.jp
evil.example.jp
Connection closed by foreign host.
$_SERVER['HTTP_HOST']
も$_SERVER['SERVER_NAME']
も同じ値を返しました。
タグを含む場合はどうでしょう?
$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /test.php HTTP/1.1
Host: <s>evil
HTTP/1.1 200 OK
Date: Fri, 06 Nov 2015 01:02:19 GMT
Server: Apache/2.4.12 (Unix) OpenSSL/1.0.1m PHP/5.5.24 mod_perl/2.0.8-dev Perl/v5.16.3
X-Powered-By: PHP/5.5.24
Content-Length: 21
Content-Type: text/html
<s>evil
<s>evil
Connection closed by foreign host.
$_SERVER['SERVER_NAME']
は何故かHTMLエスケープされています。
また、Apacheの返すSERVER_NAME
はUseCanonicalName
の設定によることが知られています。
#
# UseCanonicalName: Determines how Apache constructs self-referencing
# URLs and the SERVER_NAME and SERVER_PORT variables.
# When set "Off", Apache will use the Hostname and Port supplied
# by the client. When set "On", Apache will use the value of the
# ServerName directive.
#
UseCanonicalName Off
UseCanonicalName
がOff
の場合は、HTTPクライアントから提供された値が使われます。
On
の場合は、Apacheの設定ファイルのServerName
の設定値が使われます。
つまり、この値は環境に依存して変わってしまいますので、あまり信用しない方がよいでしょう。というのは、サーバ環境が変わると脆弱になってしまうアプリというはやはり避けた方が安全だからです。
「失敗する可能性があるものは失敗する」というマーフィーの法則が思い出されます。
CodeIgniter 3.0.3における脆弱性修正
CodeIgniterは最新の3.0.3で脆弱性修正として以下の修正をコミットしています。
diff --git a/application/config/config.php b/application/config/config.php
index 479d591..4f8f814 100644
--- a/application/config/config.php
+++ b/application/config/config.php
@@ -11,10 +11,16 @@ defined('BASEPATH') OR exit('No direct script access allowed');
|
| http://example.com/
|
-| If this is not set then CodeIgniter will try guess the protocol, domain
-| and path to your installation. However, you should always configure this
-| explicitly and never rely on auto-guessing, especially in production
-| environments.
+| WARNING: You MUST set this value!
+|
+| If it is not set, then CodeIgniter will try guess the protocol and path
+| your installation, but due to security concerns the hostname will be set
+| to $_SERVER['SERVER_ADDR'] if available, or localhost otherwise.
+| The auto-detection mechanism exists only for convenience during
+| development and MUST NOT be used in production!
+|
+| If you need to allow multiple domains, remember that this file is still
+| a PHP script and you can easily do that on your own.
|
*/
$config['base_url'] = '';
diff --git a/system/core/Config.php b/system/core/Config.php
index feea7c8..0264776 100644
--- a/system/core/Config.php
+++ b/system/core/Config.php
@@ -88,11 +88,9 @@ class CI_Config {
// Set the base_url automatically if none was provided
if (empty($this->config['base_url']))
{
- // The regular expression is only a basic validation for a valid "Host" header.
- // It's not exhaustive, only checks for valid characters.
- if (isset($_SERVER['HTTP_HOST']) && preg_match('/^((\[[0-9a-f:]+\])|(\d{1,3}(\.\d{1,3}){3})|[a-z0-9\-\.]+)(:\d+)?$/i', $_SERVER['HTTP_HOST']))
+ if (isset($_SERVER['SERVER_ADDR']))
{
- $base_url = (is_https() ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST']
+ $base_url = (is_https() ? 'https' : 'http').'://'.$_SERVER['SERVER_ADDR']
.substr($_SERVER['SCRIPT_NAME'], 0, strpos($_SERVER['SCRIPT_NAME'], basename($_SERVER['SCRIPT_FILENAME'])));
}
else
$_SERVER['HTTP_HOST']
を使っていたものを$_SERVER['SERVER_ADDR']
に変更しています。
もともと自動判定は使うべきでないと警告していたわけですが、今回、$_SERVER['HTTP_HOST']
を使ったサーバのURLの自動判定は脆弱性と判断し廃止しました。
結論
結論としては、「未検証の$_SERVER['HTTP_HOST']
や$_SERVER['SERVER_NAME']
は使ってはいけない」ということになります。
そのまま出力した場合は、Hostヘッダインジェクションによる攻撃が可能になる場合があります。
対策は、アプリの設定としてサーバのホスト名を手動で設定するか、ホワイトリストで検証することです。例えば、以下のサンプルがCodeIgniterのユーザガイドでは示されています。
$allowed_domains = array('domain1.tld', 'domain2.tld');
$default_domain = 'domain1.tld';
if (in_array($_SERVER['HTTP_HOST'], $allowed_domains, TRUE))
{
$domain = $_SERVER['HTTP_HOST'];
}
else
{
$domain = $default_domain;
}
if ( ! empty($_SERVER['HTTPS']))
{
$config['base_url'] = 'https://'.$domain;
}
else
{
$config['base_url'] = 'http://'.$domain;
}
http://www.codeigniter.com/user_guide/installation/upgrade_303.html より。
アプリのコードにホスト名をハードコードしたくない場合は、独自に環境変数を定義してそこから取得するようにするという方法も考えられます。
なお、Hostヘッダインジェクションを使った攻撃シナリオとしては、HTTPキャッシュを汚染する方法やメールに記載するURLを変更する攻撃シナリオが考えられています。
この脆弱性を利用して実際に攻撃するのはそんなに簡単ではないように思いますが、よい攻撃シナリオを思いついた人はシェアしてください!
(2015-11-10 追記)
過去に実際にあった事例として、以下がありました。
(2015-11-11 追記)
なお、Internet ExplorerにはHostヘッダを書き換えることができる脆弱性が存在するようです。
IEでは罠ページ上で302などでリダイレクトしつつLocationヘッダに%2Fなどを含めると、それらをデコードした値をHostヘッダとして送信するということで、これらを利用するとHostヘッダをHTML上にそのまま出力しているサイトではXSSが可能ということになります。
(Host:リクエストヘッダによるXSS - 葉っぱ日記 より)
まとめ
- 未検証の
$_SERVER['HTTP_HOST']
や$_SERVER['SERVER_NAME']
は使ってはいけません。 - 使えばHostヘッダインジェクションによる攻撃が可能になる場合があります。
参考
Date: 2015/11/06