XHR XSS の話.

概要

CORS が「幾つかのブラウザの先行実装」の状況から「古いブラウザではサポートされない機能」に変わりつつある頃合いなので,XHR2 が XSS の起点になりますよってお話.

そもそも XHR XSS って何よ

簡単に言うとXHR2 による XSS のことのつもり.身近なところだと,jQuery Mobile がやらかしたり大阪府警がやらかしたりした.

具体例1 jQuery Mobile

jQuery Mobile については,jQuery MobileのXSSについての解説 で解説されるとおり.

かいつまんで言うと,jQuery Mobile に location.hash の変更( hashchange イベント発火)時に,location.hash を URL とみなして読込んで,ページ内容を変更という機能があって,その読込先 URL にクロスドメインの制約がなかったので XHR2 可能な環境では,location.hash に XHR2 で読込み可能な URL を指定しておくとそのページを読み込んだだけで,悪意あるコンテンツに汚染されるというモノ.

beta2 ではクロスドメインでの読込みに明示的な許可フラグの設定が必要になっているのでひとまずは収束している.

具体例2 大阪府警の話

大阪府警のサイトでは,Prototype.jsAjax を用いて jQuery Mobile の例の様なページ遷移のない内容の切り替えが実装されている.(2012/03 現在)

この内容の切り替え先のURL に一応の制約(fromHash.match(/^[a-z_\-\.\/]+$/i))があったが,'//evil.example.com/foobar' のように '//' からはじめることで切り替え先を任意の URL にされる問題があった.

PoC が公開されてすぐに,切り替え先が '//' を含まず,かつホワイトリストに完全一致した場合にだけ遷移するように修正され収束した.

CORS 時代の XHR とのお付き合い

元々ドメインを超えたリソースへのアクセスの需要には,メジャーなブラウザでは IE が 10pp4 ( 2011年11月末 ) まで,Opera が 12.00 (Presto/2.10.232) まで対応してなく,JSONP,iframe による代替手法の供給や IE8 での専用 APIXDocumentRequest の存在により,クロスドメインのリクエストを意識的に別物として扱って来た経緯がある.

そのことが XHR.open() のリクエスト先 URL に対して,jQuery Mobile のように,そもそも意識されてなかったり,府警の例の様に '//' から始まる URL がドメインを超えるアクセスになることが見落とされたりといった状況を産んできたように思う.

じゃあどうするかというと,幾つかの状況に応じて対応していく必要がある.

外部のAPIへのリクエストを出す場合

XHR2 で外部の API のリソースを取得する場合は,想定した API ではない URL へのリクエストになってないかに気をつける.

例えば, 以下の様なコードでディレクトリトラバーサルを検出するなどやり方は色いろあると思う.

var apiurl = 'http://api.example.org/some/api/point/' + cook(params); 
var absurl = (function(path){ var a = document.createElement('a'); a.href = path; return a.href;})(apiurl); 
if ( apiurl != absurl ) { alert('something wrong'); }

同一オリジンへのリクエストを出す場合

同一オリジンへのリクエストしか行わないときでも,外部の API へのリクエストのように,リクエスト先ドメインを外部から変更できないような箇所に入れておいて http://... から始まる url を組み立てれば良い.

例えば,

function xhrget( path ){ 
  var xhr = new XMLHttpRequest(); 
  xhr.open( 'GET', path ); 
  /* more code... */ 
}

などなっていたのを

function xhrget( path ){ 
  var xhr = new XMLHttpRequest(), 
      absurl = 'http://self.example.com/' + path; 
      /* or absurl = [location.protocol, '//', location.hostname, location.port ? ':' : '', location.port, path ].join( '' ); */ 
  xhr.open( 'GET', absurl ); 
  /* more code... */ 
}

というように,リクエスト先ドメインをリテラルで指定するか location の hash や search 以外の箇所から組み立てるかすれば path が 'http:' や '//' から始まっていても意図しない外部リソースへのアクセスは発生しなくなる.

ユーザー入力の任意の URL に対して XHR したい場合

html を取得して利用する場合なら,XHR ResponseTypedocument にして取得することで script が取り除かれて動作してない状態の DOM を手に入れられるので,それで何とかする.ResponseType をサポートしてない場合や,それ以外の形式のリソース,例えば任意の JSON を取得してどうにかするとかは狂気の沙汰なので中継サーバを挟むといったブラウザの範囲外で対応するのがベターと思う.

まとめ

IE10 や Opera 12 の登場でモバイル端末がすでにそうであるように XHR がドメインを超えられるようになるので,リクエスト先には十分注意.

任意の文字列が突っ込めるようになってると当然にXSS となるし,絶対パスを渡すつもりで '/' から始まるようにしても '//' から始まるようにされるとこれも XSS になるので,外部から変更不能な形でドメインを設定してから XHR.open に渡すように.

任意の URL への XHR2 を安全に使うのは無理ゲーなのでやめる.

追記

JSON に何の問題があるのと指摘をうけたので整理と追記.

XHR XSS と書いたが,XSS となるのは以下の前提条件すべて満たす必要がある.

  1. ページの閲覧者が制御可能な箇所から XHR の完全なリクエスト先を決めていること
  2. XHR で取得したリソースを取得元のリソースの DOM かスクリプトの挙動に影響を与える方法で注入していること

jQuery Mobile の場合は,このどちらもをフレームワークが全自動で満たしていたためフレームワークを用いているすべてのページを細工された URL 経由で読み込むだけで,XSS になってた.大阪府警の例の場合は location.hash からリクエスト先を作っていたことで1つ目の条件が満たされ,取得したリソースを文字列として加工した後 HTML として元のページの DOM に注入していたことで2つ目の条件が満たされ XSS になってた.

CORS 時代の XHR とのお付き合いで書いたことのうち,「想定した API ではない URL へのリクエストにならないようにする」ことや,「リクエスト先ドメインを外部から変更できないようにする」ことは,XSS となる前提条件のうち1つ目の条件を満たせないようにするための措置.

実際には,想定した URL へのリクエストになるようにしても,前提条件の2つ目を満たさないようにはしていないので,リソースを利用する段階で API が変なモノを返してきてたり,同一ドメイン内の変なモノを拾ってしまったりで,スクリプトが実行される可能性は依然として残る.任意のURL から取得する場合よりずっと可能性は低いけど,安全に扱うための手段を講じておくほうがいい.

任意の URL に対する XHR で HTML を取得するときに ResponseType を指定するというのは,この記事での具体例1,2で示したような「別ページの内容(文字列)を HTML 断片として今のページの DOM に注入する」典型的な状況で,XSS となる前提条件の2つ目を満たせないようにするための措置.もし,XHR で取得した任意のリソースから生成した DOM を更に加工して何処かに利用した場合,XSS となることもある

JSON の場合も,任意の URL から取得してきて JSON.parse でオブジェクトにしただけでは(ブラウザにバグがあれば別だが),前提条件の2つ目を満たせてないので XSS とならない.取得元ページで加工し利用する際に,XSS になる可能性は HTML を取得してきた場合や,自ドメインからの取得の場合と同様に存在する.JSON.parse すればいいのではないか,クロスドメインで JSON を受け取っても問題ないと思う というのはその通りで,取得だけで XSS になるかのような「狂気の沙汰」と書いてたのは言いすぎだったと思う.