web-dev-qa-db-ja.com

なぜCSRF防止トークンをクッキーに入れるのが一般的なのですか?

私はCSRFの問題全体とそれを防ぐための適切な方法を理解しようとしています。 (私が読んだり、理解したり、同意したりしたリソース: OWASP CSRF防止チートシートCSRFに関する質問 。)

私が理解しているように、CSRFのまわりの脆弱性は(Webサーバの観点から)入ってくるHTTPリクエストの中の有効なセッションクッキーが認証されたユーザの希望を反映するという仮定によってもたらされます。しかし、Originドメインのすべてのクッキーはブラウザによるリクエストに魔法のように関連付けられているため、リクエスト内の有効なセッションクッキーの存在からサーバが推測できるのは、認証されたセッションを持つブラウザからのリクエストです。そのブラウザで実行されているコードについて、またはそれが実際にユーザーの希望を反映しているかどうかについては、これ以上想定することはできません。これを防ぐ方法は、ブラウザの自動Cookie処理以外の方法で行われる追加の認証情報( "CSRFトークン")をリクエストに含めることです。大雑把に言うと、セッションCookieはユーザー/ブラウザを認証し、CSRFトークンはブラウザで実行されているコードを認証します。

つまり、一言で言えば、Webアプリケーションのユーザーを認証するためにセッションCookieを使用している場合は、各応答にCSRFトークンを追加し、それぞれの(変化する)要求に一致するCSRFトークンを要求する必要があります。その後、CSRFトークンはサーバーからブラウザーへ、サーバーからサーバーへの往復を行い、要求を出しているページがそのサーバーによって承認されている(さらにはそのサーバーによって生成されている)ことをサーバーに証明します。

私の質問ですが、これは、そのラウンドトリップでそのCSRFトークンに使用されている特定の転送方法についてです。

CSRFトークンをクッキーとしてサーバーからクライアントに送信するのは一般的なようです(例えば AngularJSDjangoRails )。そして、クライアントのJavascriptにクッキーからそれを削り取らせ、それを別のXSRF-TOKENヘッダーとして添付してサーバーに返送させます。

(代替の方法は、例えば Express で推奨されているものです。サーバーによって生成されたCSRFトークンは、サーバーサイドのテンプレート拡張を介してレスポンスボディに含まれ、提供するコード/マークアップに直接添付されます。その例は、隠しフォームの入力としてサーバに返されるものです。

CSRFトークンのダウンストリームトランスポートとしてSet-Cookieを使用することがそれほど一般的なのはなぜですか。私は、これらすべてのフレームワークの作者がそれらのオプションを慎重に検討し、これを誤解していなかったと思います。しかし、一見したところ、基本的にクッキーの設計上の制限であるものを回避するためにクッキーを使用するのは当たり前のようです。実際、ラウンドトリップトランスポートとしてCookieを使用した場合(ブラウザにCSRFトークンを通知するためにSet-Cookie:サーバーの下流に、ブラウザにCookie:ヘッダーを送信してサーバーに返す)には、次の脆弱性を再導入します修正しようとしています。

私は、上記のフレームワークがCSRFトークンのためのラウンドトリップ全体のためにクッキーを使わないことを私は理解しています。彼らはSet-Cookieを下流に使用し、それから何か他のもの(例えばX-CSRF-Tokenヘッダ)を上流に使用し、これは脆弱性を排除します。しかし、下流のトランスポートとしてSet-Cookieを使用したとしても、誤解を招く可能性があり危険です。ブラウザは、本物の悪意のあるXSRFリクエストを含むすべてのリクエストにCSRFトークンを添付します。せいぜい、要求を必要以上に大きくして、最悪の場合、ある意味ではあるが見当違いのサーバーコードが実際にそれを使用しようとするかもしれませんが、それは本当に悪いことです。さらに、CSRFトークンの実際の受信者はクライアントサイドのJavascriptなので、このCookieはhttp-onlyで保護することはできません。したがって、Set-CookieヘッダーでCSRFトークンをダウンストリームに送信することは、私にとっては最適とは言えません。

224
metamatt

あなたが手を触れてきた正当な理由は、一度CSRFクッキーが受け取られたら、それが通常のフォームとAJAXPOSTの両方で使用するためにクライアントスクリプトでアプリケーションを通して使用可能であるということです。これは、AngularJSで使用されるようなJavaScriptを多用するアプリケーションでは意味があります(AngularJSを使用すると、アプリケーションが単一ページのアプリケーションになる必要はないので、CSRF値が異なるページ要求間で状態が流れる必要がある場合に役立ちます)。通常はブラウザに保存できません。

ここで説明する各アプローチの長所と短所については、典型的なアプリケーションにおける以下のシナリオとプロセスを検討してください。これらは Synchronizer Token Pattern に基づいています。

ボディアプローチの依頼

  1. ユーザーは正常にログインしました。
  2. サーバーが認証Cookieを発行します。
  3. ユーザーがクリックしてフォームに移動します。
  4. このセッションに対してまだ生成されていない場合、サーバーはCSRFトークンを生成し、それをユーザーセッションに対して格納し、それを隠しフィールドに出力します。
  5. ユーザーがフォームを送信します。
  6. サーバーは隠しフィールドがセッションに格納されたトークンと一致するかどうかを確認します。

利点:

  • 実装が簡単です。
  • AJAXと連携します。
  • フォームで動作します。
  • Cookieは実際には HTTPのみ になります。

デメリット:

  • すべてのフォームは隠しフィールドをHTMLで出力しなければなりません。
  • すべてのAJAX POSTにも値を含める必要があります。
  • ページはそれがページコンテンツに含まれるようにCSRFトークンを必要とすることを事前に知っていなければならないので、すべてのページはどこかにトークン値を含まなければならず、それは大きなサイトの実装に時間がかかるかもしれません。

カスタムHTTPヘッダ(ダウンストリーム)

  1. ユーザーは正常にログインしました。
  2. サーバーが認証Cookieを発行します。
  3. ユーザーがクリックしてフォームに移動します。
  4. ページがブラウザにロードされると、CSRFトークンを取得するためのAJAX要求が行われます。
  5. サーバーはCSRFトークンを生成し(まだセッション用に生成されていない場合)、それをユーザーセッションに対して格納し、それをヘッダーに出力します。
  6. ユーザーがフォームを送信します(トークンは非表示フィールドを介して送信されます)。
  7. サーバーは隠しフィールドがセッションに格納されたトークンと一致するかどうかを確認します。

利点:

  • AJAXと連携します。
  • Cookieは HTTPのみ になります。

デメリット:

  • ヘッダー値を取得するためのAJAX要求がないと機能しません。
  • すべてのフォームは、そのHTMLに動的に値を追加しなければなりません。
  • すべてのAJAX POSTにも値を含める必要があります。
  • CSRFトークンを取得するには、ページで最初にAJAX要求を行う必要があるため、毎回余分なラウンドトリップが発生します。
  • 追加のリクエストを節約するために、単にトークンをページに出力しているだけかもしれません。

カスタムHTTPヘッダ(アップストリーム)

  1. ユーザーは正常にログインしました。
  2. サーバーが認証Cookieを発行します。
  3. ユーザーがクリックしてフォームに移動します。
  4. このセッション用にまだ生成されていない場合、サーバーはCSRFトークンを生成し、それをユーザーセッションに対して格納し、ページコンテンツのどこかに出力します。
  5. ユーザーはAJAXを介してフォームを送信します(トークンはヘッダーを介して送信されます)。
  6. サーバーはカスタムヘッダーがセッションに格納されたトークンと一致するかどうかを確認します。

利点:

  • AJAXと連携します。
  • Cookieは HTTPのみ になります。

デメリット:

  • フォームでは動作しません。
  • すべてのAJAX POSTにヘッダーを含める必要があります。

カスタムHTTPヘッダ(アップストリームとダウンストリーム)

  1. ユーザーは正常にログインしました。
  2. サーバーが認証Cookieを発行します。
  3. ユーザーがクリックしてフォームに移動します。
  4. ページがブラウザにロードされると、CSRFトークンを取得するためのAJAX要求が行われます。
  5. サーバーはCSRFトークンを生成し(まだセッション用に生成されていない場合)、それをユーザーセッションに対して格納し、それをヘッダーに出力します。
  6. ユーザーはAJAXを介してフォームを送信します(トークンはヘッダーを介して送信されます)。
  7. サーバーはカスタムヘッダーがセッションに格納されたトークンと一致するかどうかを確認します。

利点:

  • AJAXと連携します。
  • Cookieは HTTPのみ になります。

デメリット:

  • フォームでは動作しません。
  • すべてのAJAX POSTにも値を含める必要があります。
  • CRSFトークンを取得するには、ページで最初にAJAX要求を行う必要があるため、毎回余分なラウンドトリップが発生します。

セットクッキー

  1. ユーザーは正常にログインしました。
  2. サーバーが認証Cookieを発行します。
  3. ユーザーがクリックしてフォームに移動します。
  4. サーバーはCSRFトークンを生成し、それをユーザーセッションに対して保存し、それをCookieに出力します。
  5. ユーザーはAJAXまたはHTMLフォームを介してフォームを送信します。
  6. サーバーはカスタムヘッダー(または隠しフォームフィールド)がセッションに格納されたトークンと一致するかどうかを確認します。
  7. Cookieは、CSRFトークンを取得するためのサーバーへの追加要求なしに、追加のAJAXおよびフォーム要求で使用するためにブラウザで使用できます。

利点:

  • 実装が簡単です。
  • AJAXと連携します。
  • フォームで動作します。
  • Cookie値を取得するためにAJAX要求が必ずしも必要ではありません。どのHTTPリクエストでも取得でき、JavaScriptを介してすべてのフォーム/ AJAXリクエストに追加できます。
  • CSRFトークンが取得されると、それがCookieに格納されているため、追加の要求なしに値を再利用できます。

デメリット:

  • すべてのフォームは、そのHTMLに動的に値を追加しなければなりません。
  • すべてのAJAX POSTにも値を含める必要があります。
  • Cookieはeveryリクエスト(つまり、CSRFプロセスに関与しない画像、CSS、JSなどのすべてのGET)に対して送信され、リクエストサイズが増加します。
  • Cookieを HTTPのみ にすることはできません。

そのため、cookieのアプローチはかなり動的であり、cookieの値(任意のHTTP要求)を取得して使用する簡単な方法を提供します(JSは任意のフォームに値を自動的に追加し、AJAX要求で使用できます)。ヘッダーまたはフォーム値として) CSRFトークンがセッションに対して受信されると、CSRFエクスプロイトを使用する攻撃者はこのトークンを取得する方法がないため、それを再生成する必要はありません。悪意のあるユーザーが上記の方法のいずれかでユーザーのCSRFトークンを読み取ろうとすると、これは Same Origin Policy によって防止されます。悪意のあるユーザーがCSRFトークンサーバー側を(たとえばcurlを介して)取得しようとすると、このトークンは被害者の認証セッションCookieがリクエストから失われるため、同じユーザーアカウントに関連付けられません。サーバー側が被害者のセッションに関連付けられることはありません。

Synchronizer Token Pattern 同様に Double Submit Cookie CSRF防止方法もあります。もちろん、これはクッキーを使用してCSRFトークンのタイプを格納します。 CSRFトークンにサーバー側の状態を必要としないため、これは実装が簡単です。このメソッドを使用する場合、実際にはCSRFトークンが標準の認証Cookieになる可能性があります。この値は通常のリクエストと同様にCookie経由で送信されますが、値は隠しフィールドまたはヘッダーにも繰り返されます。そもそも値を読むことはできません。ただし、HttpOnlyとマークされて認証Cookieが保護されるように、認証Cookie以外の別のCookieを選択することをお勧めします。ですから、これが、Cookieベースの方法を使用したCSRF防止を見つけるもう1つの一般的な理由です。

216
SilverlightFox

攻撃者はCookieの値を読み取ることができず、サーバー側のCSRF検証で要求される場所にCookieを配置できないため、Cookieを使用してクライアントにCSRFトークンを提供しても攻撃を成功させることはできません。

攻撃者は、リクエストヘッダーに認証トークンCookieとCSRF Coo​​kieの両方を使用してサーバーにリクエストを送信することができます。しかし、サーバーはCSRFトークンを要求ヘッダー内のCookieとして探しているのではなく、要求のペイロードを探しています。そして攻撃者がペイロードのどこにCSRFトークンを入れるべきかを知っていても、それをそこに置くために彼らはその値を読まなければならないでしょう。しかし、ブラウザのクロスオリジンポリシーにより、ターゲットWebサイトからCookieの値を読み取ることはできません。

同じ論理は認証トークンクッキーには当てはまりません。なぜならサーバはリクエストヘッダでそれを期待しており、攻撃者はそれを置くために特別なことをする必要がないからです。

36
Tongfa

答えとして私の最もよい推測:CSRFトークンをサーバからブラウザに転送する方法についてこれらの3つのオプションを検討してください。

  1. リクエストボディ内(HTTPヘッダーではありません)。
  2. Set-Cookieではなく、カスタムHTTPヘッダー内。
  3. Cookieとして、Set-Cookieヘッダーに。

最初のもの、リクエストボディ( 質問でリンクしたExpressチュートリアル で示されています)は、多種多様な状況への移植性が低いと思います。すべてのHTTPレスポンスが動的に生成されるわけではありません。トークンを生成されたレスポンスに入れる必要がある場所は、(隠れた形式の入力、JSコードの断片、または他のJSコードからアクセス可能な変数など、さまざまな場合があります) CSRFトークンを入れるため)そのため、カスタマイズは可能ですが、#1は、万能のアプローチをとるのは困難です。

2番目のカスタムヘッダーは魅力的ですが、実際には機能しません。 JSは起動したXHRのヘッダーを取得できますが、読み込んだページのヘッダーは取得できません

3番目のもの、つまりSet-Cookieヘッダーによって運ばれるCookieは、すべての状況で使いやすいアプローチとして残されています(誰のサーバーでも要求ごとにCookieヘッダーを設定できます。データは要求本体にあります。したがって、その欠点にもかかわらず、フレームワークを広く実装するための最も簡単な方法でした。

8
metamatt

セッションクッキー(これは一種の標準です)以外に、私は余分なクッキーを使用したくありません。

私は、多数のAJAX要求を使用して単一ページのWebアプリケーション(SPA)を構築するときに私には有効な解決策を見つけました。注:私はサーバー側のJavaとクライアント側のJQueryを使用していますが、不思議なことではないので、この原則はすべての一般的なプログラミング言語で実装できると思います。

余分なクッキーを使わないで私の解決策は簡単です:

クライアント側

ログインが成功した後にサーバーから返されたCSRFトークンをグローバル変数に格納します(もちろん、グローバルなストレージの代わりにWebストレージを使用する場合)。各AJAX呼び出しでX-CSRF-TOKENヘッダーを提供するようにJQueryに指示してください。

メインの "index"ページには、このJavaScriptコードが含まれています。

// Intialize global variable CSRF_TOKEN to empty sting. 
// This variable is set after a succesful login
window.CSRF_TOKEN = '';

// the supplied callback to .ajaxSend() is called before an Ajax request is sent
$( document ).ajaxSend( function( event, jqXHR ) {
  jqXHR.setRequestHeader('X-CSRF-TOKEN', window.CSRF_TOKEN);
});

サーバ側

正常にログインしたら、ランダムな(そして十分に長い)CSRFトークンを作成し、これをサーバー側セッションに格納してクライアントに返します。 X-CSRF-TOKENヘッダー値とセッションに格納されている値を比較して、特定の(機密性の高い)着信要求をフィルタリングします。これらは一致する必要があります。

敏感なAJAX呼び出し(POSTフォームデータとGET JSONデータ)、およびそれらをキャッチするサーバーサイドフィルタは/ dataservice/*パスの下にあります。ログイン要求はフィルタをヒットしてはいけないので、これらは別のパスにあります。 HTML、CSS、JS、および画像リソースに対する要求も/ dataservice/*パス上にないため、フィルタリングされません。これらには何も秘密がなく、害を及ぼすこともできないので、これで問題ありません。

@WebFilter(urlPatterns = {"/dataservice/*"})
...
String sessionCSRFToken = req.getSession().getAttribute("CSRFToken") != null ? (String) req.getSession().getAttribute("CSRFToken") : null;
if (sessionCSRFToken == null || req.getHeader("X-CSRF-TOKEN") == null || !req.getHeader("X-CSRF-TOKEN").equals(sessionCSRFToken)) {
  resp.sendError(401);
} else 
  chain.doFilter(request, response);
}
0