これは「脆弱性"&'<<>\ Advent Calendar 2015」の12月19日の記事です。
この記事では Chrome 46 で修正された CVE-2015-6759 を紹介します。この脆弱性は先月の AVTOKYO 2015 でも披露したので、ご存じの方もいるかもしれません。
この脆弱性は、data: と blob: という2つの特殊なURLを組み合わせることにより、Chrome のオリジン判定を誤らせ、結果として、ネットワーク上から file: スキームの localStorage のデータを読み出すことができるというものです。仮にユーザが file: スキームの localStorage にトークンなどの機密情報を格納している場合、悪意のあるリンクを開くだけでそれらの情報が盗まれてしまいます。
この脆弱性のメカニズムはやや複雑ですので、data: URL と blob: URL の性質から順を追って説明します。これらをある程度知っている方は、前半部分を読み飛ばしても構いません。
data: URL はその名のとおり URL です。元々は小さな画像などのリソースを HTML にインラインで埋め込むことを目的として提案されました。 data: URL を使えば、例えば以下の PNG 画像を data: というスキームの URL として表現できるようになります。
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAuUlEQVQ4jc3VMQqEMBAF0E+KdB4gB7CyT51jeI7cQTuvYUobsRU8gofwBukCfyuFxWB2lyl24HfDg5DJBCzUtm1clqXUdhVKDSEEaq05jqMMOE0TAVApxWEY5MAz3numlH4H53l+AwGwbVvGGPOgtZZPqev6BgKgc47HcdzBXPOnaZqG+77LgQBojOG6rnIgAFZVdY2VCHim73tZsOu6Pzyy6KVkx0Z8sMWfXgkUXw7i60t8wX77BbwAqODxqxsojaUAAAAASUVORK5CYII=
data URL は通常の URL と同様に <img> タグなどで参照できます。アイコン画像のようにサイズの小さなリソースであれば、 data: URL として HTML に直接埋め込むことで TCP のコネクションを減らすことができ、通信オーバーヘッドの削減が期待できるかもしれません。
data: URL で表すことができるのは画像などのリソースだけではありません。URL の先頭を data:text/html
とすれば、HTML そのものを表すこともできます。
しかし、ここで Web セキュリティの原則である 同一オリジンポリシー (SOP) を思い出してください。SOP は、URL の「スキーム」+「ホスト名」+「ポート番号」の三つを組み合わせたものを「オリジン」と定義し 、同じオリジンの URL を同一の Web サイトと見なして DOM などのデータを共有可能とします。
この SOP の仕組みをそのまま data: URL に適用した場合、同じ data: スキームを持つ複数の URL がその生成元のサイトに関わらず同じオリジンになってしまいます。これは例えば、google.com によって生成された data: URL と、悪意のあるサイトが生成した data: URL が同一の Web サイトとして扱われ、それらの中でユーザのデータなどが共有されてしまうということを意味します。仮にこのような仕様だとしたら、セキュリティ上とてもまずそうです。
それでは、data: URL のオリジンは一体どうあるべきなのでしょうか?
data: URL の仕様は 1998 年に RFC 2397 として提案されました。しかし、この仕様には data: URL で表された HTML をブラウザがどのように扱うかは明記されていません。このため、ブラウザベンダは data: URL のあるべき振る舞いを各々で定めて実装してきました。この仕様の独自解釈が、RFC の登場から 17 年が過ぎた今もなおブラウザ上で様々な脆弱性を引き起こす温床となっているのです。
それでは、主要ブラウザごとの data: URL の振る舞いを見てみましょう。以下の表は、data: URL の HTML を様々な方法で開き、その際にどのようなオリジンがページへ割り当てられるかを示したものです (重要なところだけを抜粋してます)。
a[href] | iframe[src] | 30x redirect | meta refresh | URLバーに手入力 | お気に入り | |
---|---|---|---|---|---|---|
IE 11 | 遷移不可 | 遷移不可 | 遷移不可 | 遷移不可 | 遷移不可 | 遷移不可 |
Edge | 遷移不可 | 独自のオリジン | 遷移不可 | 遷移不可 | 遷移不可 | 遷移不可 |
Firefox (42.0a1) | 呼出元を継承 | 呼出元を継承 | 独自のオリジン | 独自のオリジン | 独自のオリジン | 現在アクティブなタブのオリジンを継承 |
Safari (8) | 独自のオリジン | 独自のオリジン | 独自のオリジン | 独自のオリジン | 独自のオリジン | 独自のオリジン |
Chrome (43) | 独自のオリジン | 独自のオリジン | 遷移不可 | 独自のオリジン | 独自のオリジン | 独自のオリジン |
Firefox では、data: URL はその URL を開いた元となるサイトのオリジンを基本的に継承します。これに対し、Chrome では URL の開き方に関わらず独自のオリジンが割り当てられます。余談ですが、Chrome は、「スキーム」+「ホスト名」+「ポート番号」のみでなく、合計で 12個の因子 を複合的に判断してページのオリジンを決定しているようです。
前置きが長くなりますが、前提となる技術の話をもう少し続けます。最近のブラウザは、data: URL の他に blob: URL というものをサポートしています。これらは任意のリソースをURLで表現できるという点は同じですが、blob: URL はブラウザの内部で動的に生成されるもので、ブラウザを閉じると URL が失効してしまう点が data: URL とは異なります。
blob: URL は JavaScript の URL.createObjectURL
というメソッドを用いて次のように生成できます。
var text = "<h1>Hello</h1>"
var blob = new Blob([text], {type : 'text/html'});
var url = URL.createObjectURL(blob);
これにより生成された URL は次のようになります。
ここでポイントは、blob: というスキームの直後には、その blob: URL を生成したオリジンが含まれるという点です。上の図では、私が google.com を開いた状態でデベロッパーツールから URL を生成したため、オリジンは blob:https%3A//www.google.com
となっています。
このオリジンはある条件下で null となり、 blob:null
で始まる URL が生成されます。
私が認識しているのは次のケースです。
-
data: URL のページ上で blob: URL が生成された場合
-
iframe sandbox の中で blob: URL が生成された場合
-
ローカルデータ (file: スキーム) のページ上で blob: URL が生成された場合
1番目は先述の data: URL です。Chrome は data: URL の HTML に独自のオリジンを割り当てます。この中で生成された blob: URL もまた独自のオリジンを引き継ぐ形となり、そのオリジンは null となります。
2番目の iframe sandbox も仕組みは同じです。<iframe > タグに sandbox 属性を指定した場合、その中で表示されたページには独自のオリジンが割り当てられます。その中で生成された blob: URL のオリジンも null となります。
3番目は私が今回の脆弱性に気付くヒントになった点でした。file: スキームで生成された blob: URL は data:file
というオリジンで良いと思うのですが、オリジンは null となります。
しかし、たとえ上記のオリジンが全て null になるとはいえ、これら三つのケースで生成された blob: URL はどれもその生成元が異なります。これらの URL が実際に動作するオリジンは全て異なるべきです。
さてここからが本題です。私が発見した脆弱性は、data: URL の HTML 上で生成した blob: URL の HTML から、file: スキームの localStorage のデータを盗み出すことができるというものでした。これはつまり、攻撃者がユーザを悪意のある data: URL に誘導するだけで、ユーザの端末上にあるデータを盗み出すことができることを意味します。
この脆弱性の実証コードは以下のとおりです。
data:text/html;charset=utf-8;base64,PGJvZHk+DQo8c2NyaXB0Pg0KCXZhciB0ZXh0ID0gIlx1MDAzY3NjcmlwdFx1MDAzZWFsZXJ0KGxvY2FsU3RvcmFnZS5nZXRJdGVtKCd0b2tlbicpKVx1MDAzYy9zY3JpcHRcdTAwM2UiOw0KCXZhciBibG9iID0gbmV3IEJsb2IoW3RleHRdLCB7dHlwZSA6ICd0ZXh0L2h0bWwnfSk7DQoJdmFyIHVybCA9IFVSTC5jcmVhdGVPYmplY3RVUkwoYmxvYik7DQoJd2luZG93LnRvcC5sb2NhdGlvbiA9IHVybDsNCjwvc2NyaXB0Pg0KPC9ib2R5Pg==
この data: URL の Base64 をデコードすると以下の HTML 文字列が現れます。内部では URL.createObjectURL
を用いて localStorage を読み出す blob: URL を生成し、そこに遷移していることが読み取れます。
<body>
<script>
var text = "\u003cscript\u003ealert(localStorage.getItem('token'))\u003c/script\u003e";
var blob = new Blob([text], {type : 'text/html'});
var url = URL.createObjectURL(blob);
window.top.location = url;
</script>
</body>
報告当初、この脆弱性の深刻度は High という判定を受けましたが、最終的な判定は Medium となりました。Google はこの理由を次のように述べています。
That said, there are too many preconditions for exploitation (e.g. needs file to be stored via file:// and then that should generate tokens in localstorage), so we couldn't agree on more than $1,000 for this particular report.
確かに、Chrome を利用するユーザの多くは file: スキームの localStorage に機密情報など保存しません。ですので、この脆弱性を悪用して機密情報を盗み出すためには、事前に何らかの方法を用いて localStorage に機密情報を保存させる必要があります。この条件を成立させることは困難であろうということから、Google はこの脆弱性の深刻度を Medium と評価したのです。
この評価は Chrome のみに限って言えば納得できます。しかし、この脆弱性は Android 4.4 以降の WebView にも影響するのです。
Android アプリの中には、アプリのパッケージ (APK) に含まれる HTML を WebView で開き、それをアプリの UI とするものがあります。そして、これらのアプリが APK の HTML を開くときには file: スキームが使用されます。さらに、こうした仕組みで作られたアプリの中には、file: スキームの localStorage にユーザの認証情報やトークンを格納しているものが少なからず存在します。ある有名なアプリは、'authToken' という key で API のトークンを localStorage に格納していました。
これはつまり、こうした仕組みを持つAndroidアプリが攻撃の標的とされた場合、WebView を通じて トークンなどの情報が盗まれる可能性があることを意味します。このように WebView に与える影響までを考慮に入れると、深刻度は Medium より上になるのではないでしょうか。
Google にはこうした付加情報を伝えたのですが、残念ながら、明確なコメントは得られませんでした。
data: URL と blob: URL を組み合わせたこの攻撃手法は、私が知る限り前例のないものです。私はこの脆弱性を見つけられたことに対する達成感の余韻が続き、しばらくの間は満たされた気持ちになりました。しかし、2015年12月15日、ある事件が起こります。らまっこ氏(@llamakko_cafe)がこの手法をさらに発展させて、 Firefox の深刻な脆弱性 を見つけてしまったのです。
これはもう功績の上に胡坐をかいていた自分を悔いて素直に反省するしかありません。来年はもっと真摯に脆弱性と向き合っていこうと、襟を正すことのできた出来事でした。