Hatena::ブログ(Diary)

葉っぱ日記 このページをアンテナに追加

2013-03-02

[] XMLHttpRequestを使ったCSRF対策  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やパスワード再入力のような、ユーザーにとっての面倒くさい手続きが不要
  • 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:ヘッダが付与される

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

hasegawayosukehasegawayosuke 2013/03/02 07:36 X-From の内容は別に location.href である必要ないね。

momijiamemomijiame 2013/03/02 10:54 実際の実装であれば Java JAX-RS の Jersey フレームワークに同じ手法を用いたフィルタが存在しますね。
以下のサイトに、実装にあたって参照しているペーパーなども載っています。
http://blog.alutam.com/2011/09/14/jersey-and-cross-site-request-forgery-csrf/
http://java.net/projects/jersey/sources/svn/content/trunk/jersey/jersey-server/src/main/java/com/sun/jersey/api/container/filter/CsrfProtectionFilter.java?rev=5392
# ヘッダの名前には "X-Requested-By" を使って、中身の検証まではしていませんね

Takahiro HozumiTakahiro Hozumi 2013/03/02 11:58 JSONで受け取って、CSRFの時にはパーサーでコケるはずってのは対策にならないですか。
これもクライアントでJavaScript必要ですが。

hasegawayosukehasegawayosuke 2013/03/02 12:46 実際の実装もあるんですね。ありがとうございます。

hasegawayosukehasegawayosuke 2013/03/02 12:49 JSONで受け取って…というのはちょっと詳細わかりませんが、罠ページがjson投げたらCSRFしたりしないんでしょうか?

Takahiro HozumiTakahiro Hozumi 2013/03/02 13:31 CSRFって罠ページにターゲットURLに向けたformを用意してリクエストを投げる方法しかないと思います、多分。
formのPOSTだとJSON形式じゃ投げれないので、罠ページはJSON投げれないと考えていいのではないでしょうか。

hasegawayosukehasegawayosuke 2013/03/02 14:05 罠ページがXHR使ってPOSTすればJSON投げられますよ。

momijiamemomijiame 2013/03/02 15:38 横からすみません、罠ページではブラウザのSame-Originポリシーが働くためXHRは投げられないんじゃないでしょうか?
1. 罠ページがターゲットのドメインと異なる
2. サービス側が"Access-Control-Allow-Origin"ヘッダを使ってクロスドメインの通信を許可していない
というのが前提になりますが。
(何か勘違いしていたらすみません)

hasegawayosukehasegawayosuke 2013/03/02 15:55 Access-Control- Allow-Origin の有無に関わらず、罠ページはXHRでPOSTを発行することはできます。結果が読めないだけで。

momijiamemomijiame 2013/03/02 16:20 おお、手元で試してみたところ、確かにその通りですね。知りませんでした。

Takahiro HozumiTakahiro Hozumi 2013/03/02 18:43 > 罠ページがXHR使ってPOSTすればJSON投げられますよ。

調べたところ、確かに罠サイトがJSONをXHRで投げる事はできるみたいですが、まずPOSTではなくOPTIONSというリクエストを行いそのレスポンスのヘッダーを見て、クロスサイトのリクエストが許可されてるかブラウザが判断するみたいです。
なので、サーバー側が許可しない限り、POSTでJSONを受け取る事はないのではないかと思います。

hasegawayosukehasegawayosuke 2013/03/02 20:12 カスタムヘッダがついてなければ Preflight は飛ばないですね。この方式だとフレームワークが preflight に対応していても、していなくても統一された対策手法が採れます。

Takahiro HozumiTakahiro Hozumi 2013/03/02 21:56 >カスタムヘッダがついてなければ Preflight は飛ばないですね。

PreflightというのはOPTIONSでのリクエストのことでしょうか。
ChromeでこのページのConsole開いてカスタムヘッダを付けずに localhost へxhrを飛ばしてみましたが、OPTIONSでリクエストが行われるようです。
https://gist.github.com/hozumi/5070846
何か勘違いしていましたらすみません。

YUTAYUTA 2015/07/19 21:17 どうも初めまして!YUTAと申します。
POSTされた先のページをPHPで書いてみました。こちらで問題無いかご教授いただきたいです。大変お手数ですがよろしくお願いいたします。

<?php
//Ajax通信でない場合
if (!(isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
){ die ('このページを直接ロードしないでください。'); }

//リクエストヘッダーの確認
$host = $_SERVER["HTTP_HOST"];
$hasXForm = false;
foreach (getallheaders() as $name => $value)
{
if($name === "Host" || $name === "X-From" || $name === "Origin")
{
if(strpos($value, $host) === FALSE) exit;
if($name === "X-From") $hasXForm = true;
}
}

if(!$hasXForm) exit;