連載
» 2014年03月17日 18時00分 UPDATE

HTML5時代の「新しいセキュリティ・エチケット」(3):知っていれば恐くない、XMLHttpRequestによるXSSへの対応方法 (1/2)

“新しいXSS”は知識こそが対策の第一歩。基本の対策を行うことが重要です。XMLHttpRequestによるXSSも例外ではありません。

[はせがわようすけ(ネットエージェント株式会社),@IT]
「HTML5時代の『新しいセキュリティ・エチケット』」のインデックス

連載目次

 皆さんこんにちは。ネットエージェントのはせがわようすけです。前回は、同一オリジンポリシーを突破する攻撃の代表的事例であるXSSについて、特にDOM based XSSと呼ばれるものについて解説しました。今回はその続きとして、XMLHttpRequestによるXSSを解説します。

XHR Level 2によるリモートからのコード挿入によるXSS

 従来、XMLHttpRequest(以下、XHR)は、表示しているドキュメントと同じオリジン(オリジンについては第1回を参照)としか通信できませんでしたが、現在の主要なブラウザーではXHR Level 2と呼ばれる実装により、オリジンを超えて通信することが可能になっています。

 これは、JavaScriptによるいわゆるAjaxなアプリケーションの可能性を大きく広げると同時に、攻撃者にとっても攻撃対象となるサイトと攻撃者自身の用意した罠サイトの間での通信を可能にし、攻撃の幅を広げるものにもなっています。また、XHRがオリジンを超えて通信しないという時代に設計されたかつてのWebアプリケーションには、クロスオリジン通信が可能になったXHR Level 2の出現、すなわちブラウザーのバージョンアップとともに、ある日突然脆弱になったようなものもあります。

 例えば、自サイトとの通信を前提とし通信先をURL内の「#」(ハッシュ)以降に指定し、「http://example.jp/#/news」のようにアクセスすることで、XHRにより随時ページ内のコンテンツの一部を書き換えるという、以下のようなJavaScriptコードがあったとします。

// 自サイトとの通信を前提としたコード(脆弱な例)
var url = location.hash.substring( 1 ) || "/news";
var xhr = new XMLHttpRequest();
xhr.open( "GET", url, true );
xhr.onreadystatechange = function(){
    if( xhr.readyState == 4 && xhr.status == 200 ){
        div.innerHTML = xhr.responseText; // 取得したコンテンツを表示
    }
};
xhr.send( null );

 このようなコードは、XHRがドキュメントと同じオリジンとしか通信できなかったころには全く問題がありませんでしたが、XHRがオリジンを超えて通信できるようになった現在のブラウザーではXSSが存在することになります。

 攻撃者がユーザーを「http://example.jp/#//evil.example.com/」などのURLに誘導すると、XHRの通信先として「//evil.example.com/」が使用されます。「//」で始まるURLは、現在のURLと同じプロトコルスキームを使うことを意味するので、XHRは「http://evil.example.com/」からコンテンツを取得し、それが「http://example.jp/」上にHTMLとして挿入される、典型的なDOM based XSSが発生します。

 この例では、「div.innerHTML」へ代入しているので、攻撃者の用意したスクリプトが動作することになりますが、前回説明したDOM based XSSへの対策として、文字列のエスケープやテキストノードを用いてのDOM操作を行っていた場合はどうなるでしょうか。

 この場合、確かにスクリプトは動作しませんが、それでも攻撃者の用意した偽のコンテンツがWebサイト上に表示されることに変わりありません。例えばニュースリリースを公開するページのように、Webサイト訪問者に信頼できる情報を与えることが目的の場合には大きな問題となります。

本質的な対策は「リクエスト先を固定」すること

 本質的な対策としては、意図しないサイトに対して通信しないよう、XHRのリクエスト先を固定する方法があります。

// 安全な例。通信先を事前に固定のリストとして保持
var page = decodeURIComponent( location.hash.substring(1) );
var pages = { "/news":"/news", "/comment":"/comment"}; // 通信先の一覧
var url = pages[ page ] || "/news"; //undefinedのときは/newsを取得
var xhr = new XMLHttpRequest();
xhr.open( "GET", url, true );
xhr.onreadystatechange = function(){
    if( xhr.readyState == 4 && xhr.status == 200 ){
        div.innerHTML = xhr.responseText;
    }
}

 このように、XHRの通信先を固定のリストをあらかじめコード内で保持しておくことで、他のサイトと通信してしまう余地を完全に排除することができます。

通信先のURLをチェックする方法はアリ? ナシ?

 一方、XHRの通信前にURLが自サイトのものかどうかを検査するという方法では、どうしても検査の漏れが発生する可能性があるため、あまりよい方法とはいえません。

 例えば、通信先URLが自サイト内かどうかを事前に確認する以下のようなコードがあったとします。

// 脆弱な例。事前にURLが自サイト内かどうかを確認しているつもり
var url = location.hash.substring( 1 ) || "/news";
if( url.match( /^\/[^\/]/ ) ){  // 先頭が「/」、2文字目が「/以外」のときのみ通信
    var xhr = new XMLHttpRequest();
    xhr.open( "GET", url, true );
    xhr.onreadystatechange = function(){
        if( xhr.readyState == 4 && xhr.status == 200 ){
            div.innerHTML = xhr.responseText; // 取得したコンテンツを表示
        }
    };
    xhr.send( null );
}

 この例では、通信先のURLが例えば「/news」のように、正規表現によるチェックを行い、先頭が「/」で2文字目が「/以外」という場合にのみXHRによってコンテンツを取得、表示することを意図しています。ところが実際にはブラウザーによっては「/\evil.example.com/」のような表記によっても「//evil.example.com/」と同義に取り扱うものもあり、攻撃者はURLの検査箇所をうまく回避してXSS攻撃に成功します。

 このように、URLが想定したサイト内であるか否かを調べるのは実は意外と困難です。そしてサイト内に一つでもオープンリダイレクターが存在する場合は、そのオープンリダイレクターを経由して攻撃者サイトのコンテンツを指し示すことにより、URLの検査を回避することも可能になっていまします。

 繰り返しになりますが、こういった脆弱性を防ぐために、XHRの通信先はあらかじめコード内で固定のリストとして保持しておくことが望ましいといえるでしょう。

       1|2 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。