XMLHttpRequestを使ったCSRF対策
合わせて読んでください:Flashと特定ブラウザの組み合わせでcross originでカスタムヘッダ付与が出来てしまう問題が未だに直っていない話 (2014-02/07)
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を設定している。
サーバ側では送られてきた内容について、以下を検査する。
- Host: リクエストヘッダが自身のホスト名を指していること (2013/03/04追記。これを見ておかないとDNS Rebindingで突破される可能性があるとkanatokoさんから指摘あり)
- X-From: リクエストヘッダが付与されていること。
- X-From: リクエストヘッダの値が、想定しているHTMLページのアドレスであること。(2013/03/03追記:この確認は必須じゃない)
- Origin: リクエストヘッダが以下のいずれかであること:
以上の条件を満たす場合、CSRFではない正規のリクエストとして処理を続行してよい。
CSRFのために攻撃者が罠ページを用意し、罠ページ内からformのsubmitによってリクエストを発行した場合には X-From:ヘッダを付与することはできない。また、罠ページ内からXHRを経由してリクエストを発行した場合には、Origin:ヘッダが罠サイトのオリジンを示す。これらにより、リクエストが正規の手続きを経て発行されたものか、罠ページから発行されたものかをサーバ側では判断できるという仕組みである。
この方式によるメリットは
- Cookieやhiddenによるトークンを使用せず、サーバ側でセッション機構が不要
- Captchaやパスワード再入力のような、ユーザーにとっての面倒くさい手続きが不要
- Cookie、Captcha、hiddenによるトークンのような「秘密の情報」を使用しないので、ネットワーク上で盗聴されてもCSRFされることがない*2
- Cookieを使用しないので、クッキーモンスターの影響を受けない。
- リファラを使用しないので、リファラを送信しない環境でも利用可能。
などがある。一方でデメリットとしては、JavaScriptが必須ということである。
また、IE6-IE9を見捨てるのであれば、フォームのHTMLを設置するサイトとPOST先サイトを別のオリジンに配置するその場合に、Cookieやセッションの引き継ぎなどが不要というメリットもある。(2013/03/03追記:もちろんサーバ側はpreflightリクエストに対応する必要がある。2013/03/05修正:IE8->IE9)
ただし、こういう実装を実際には見たことがないので、私が想定できていないような落とし穴があるのかもしれない(これ書いてる今、すごく眠たいし)。
*1:Google Chromeでは同一オリジンでもPOSTではOrigin:ヘッダが付与される