XMLHttpRequestを使ったCSRF対策

XMLHttpRequestを使うことで、Cookieリファラ、hidden内のトークンを使用せずにシンプルにCSRF対策が行える。POSTするJavaScriptは以下の通り。(2013/03/04:コード一部修正)

function post(){
    var s = 
        "mail=" + encodeURIComponent( document.getElementById("mail").value )  +
        "&msg=" + encodeURIComponent( document.getElementById("msg").value );
    var xhr = new XMLHttpRequest();
    xhr.open( "POST", "/inquiry", true );
    xhr.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" );
    xhr.setRequestHeader( "X-From", location.href );
    xhr.onreadystatechange = function(){ /* ... */};
    xhr.send( s );
    return false;
}

ごく通常のXHRを使ったPOSTだが、setRequestHeaderにより X-From: リクエストヘッダに自身のURLを設定している。
サーバ側では送られてきた内容について、以下を検査する。

  1. Host: リクエストヘッダが自身のホスト名を指していること (2013/03/04追記。これを見ておかないとDNS Rebindingで突破される可能性があるとkanatokoさんから指摘あり)
  2. X-From: リクエストヘッダが付与されていること。
  3. X-From: リクエストヘッダの値が、想定しているHTMLページのアドレスであること。(2013/03/03追記:この確認は必須じゃない)
  4. Origin: リクエストヘッダが以下のいずれかであること:
    • Origin: リクエストヘッダがついていない。あるいは
    • Origin: リクエストヘッダがついており*1、X-From: で示されるURLとオリジンが同一(想定しているHTMLページのオリジン)であること。

以上の条件を満たす場合、CSRFではない正規のリクエストとして処理を続行してよい。
CSRFのために攻撃者が罠ページを用意し、罠ページ内からformのsubmitによってリクエストを発行した場合には X-From:ヘッダを付与することはできない。また、罠ページ内からXHRを経由してリクエストを発行した場合には、Origin:ヘッダが罠サイトのオリジンを示す。これらにより、リクエストが正規の手続きを経て発行されたものか、罠ページから発行されたものかをサーバ側では判断できるという仕組みである。

この方式によるメリットは

  • Cookieやhiddenによるトークンを使用せず、サーバ側でセッション機構が不要
  • Captchaやパスワード再入力のような、ユーザーにとっての面倒くさい手続きが不要
  • CookieCaptcha、hiddenによるトークンのような「秘密の情報」を使用しないので、ネットワーク上で盗聴されてもCSRFされることがない*2
  • Cookieを使用しないので、クッキーモンスターの影響を受けない。
  • リファラを使用しないので、リファラを送信しない環境でも利用可能。

などがある。一方でデメリットとしては、JavaScriptが必須ということである。
また、IE6-IE9を見捨てるのであれば、フォームのHTMLを設置するサイトとPOST先サイトを別のオリジンに配置するその場合に、Cookieやセッションの引き継ぎなどが不要というメリットもある。(2013/03/03追記:もちろんサーバ側はpreflightリクエストに対応する必要がある。2013/03/05修正:IE8->IE9)

ただし、こういう実装を実際には見たことがないので、私が想定できていないような落とし穴があるのかもしれない(これ書いてる今、すごく眠たいし)。

*1:Google Chromeでは同一オリジンでもPOSTではOrigin:ヘッダが付与される

*2:ネットワーク上で攻撃者が改ざんできる場合はもちろんCSRFされる可能性がある。