クロスサイトリクエストフォージェリの防止
Cross-Site Request Forgery Prevention Cheat Sheet
- 原典
- Cross-Site Request Forgery Prevention Cheat Sheet(OWASP Cheat Sheet Series)
- 原文
- GitHub 上の Markdown
- 底本
bac04fb5(2026-07-29 時点)- ライセンス
- CC BY-SA 4.0(原典と同一。訳文も同ライセンスで再配布できます)
はじめに
クロスサイトリクエストフォージェリ(CSRF)攻撃は、悪意あるサイト、メール、ブログ、インスタントメッセージ、プログラムが、認証された利用者のブラウザを騙して、信頼されたサイト上で望まれない動作を実行させるときに生じる。 標的の利用者がそのサイトで認証されている場合、保護されていない標的のサイトは、正当な認可されたリクエストと偽造された認証済みのリクエストを区別できない。
ブラウザのリクエストはセッションの Cookie を含むすべての Cookie を自動的に含めるので、適切な認可が使われていないかぎりこの攻撃は成立する。 つまり標的のサイトのチャレンジとレスポンスの仕組みが、要求者の身元と権限を検証していないということである。 実際のところ CSRF 攻撃は、被害者の知らないうちに(通常は認可されていない動作が完了したあとまで気付かれずに)、被害者のブラウザを通じて標的のシステムに攻撃者が指定した機能を実行させる。
ただし CSRF 攻撃が成功しても、悪用できるのは脆弱なアプリケーションが公開する機能と、その利用者の権限に限られる。 利用者の資格情報に応じて、攻撃者は送金、パスワードの変更、認可されていない購入、標的アカウントの権限昇格、あるいはその利用者に許されているあらゆる動作を行える。
要するに、CSRF に対する防御には次の原則に従うべきである。
重要:クロスサイトスクリプティング(XSS)は CSRF のあらゆる緩和手法を無力化しうることを忘れない。 XSS の脆弱性は CSRF の保護を回避しうるが、認証に Cookie を用いる Web アプリケーションにとって CSRF トークンはなお不可欠である。 アプリケーションにおける CSRF 保護の最良の方法を決めるには、クライアントと認証の方式を考慮する。
- XSS の欠陥を防ぐ詳しい指針はクロスサイトスクリプティングの防止を参照する。
- まず、使っているフレームワークに組み込みの CSRF 保護があるかを確認し、それを使う。
- フレームワークに組み込みの CSRF 保護がなければ、状態を変えるすべてのリクエスト(サイト上で動作を引き起こすリクエスト)に CSRF トークンを加え、バックエンドで検証する。
- ソフトウェアが現代のブラウザのみを対象とするなら、後述の代替の選択肢とあわせて Fetch Metadata のヘッダに依拠して、サイトをまたぐ状態変更のリクエストを遮断できる。
- 状態を持つソフトウェアはシンクロナイザトークンパターンを使うべきである。
- 状態を持たないソフトウェアは二重送信 Cookieを使うべきである。
- API 主導のサイトで
<form>タグを使えない場合は、独自のリクエストヘッダを使うことを検討する。 - 多層防御の緩和策の節から少なくとも一つを実装する。
- **SameSite の Cookie 属性をセッションの Cookie に使える。**ただしドメインを指定して Cookie を設定しないよう注意する。その動作は安全上の脆弱性を持ち込む。そのドメインのすべてのサブドメインが Cookie を共有することになり、あるサブドメインが自分の管理外のドメインへの CNAME を持つ場合はとくに問題になる。
- 非常に機微な操作には利用者の操作に基づく保護の実装を検討する。
- 標準のヘッダによる生成元の検証を検討する。
- 状態を変える操作に GET のリクエストを使わない。
- 何らかの理由でそうする場合は、それらのリソースを CSRF から保護する。
組み込みの、あるいは既存の CSRF の実装
独自のトークンや Fetch Metadata の実装を作る前に、使っているフレームワークやプラットフォームがすでに使える CSRF 保護を提供していないかを確認する。 組み込みの防御は一般に望ましい。 フレームワークの作者によって保守され、微妙な実装の誤りのリスクを減らすからである。 たとえば次のものがある。
- .NET は 組み込みの保護を使い、CSRF に脆弱なリソースへトークンを加えられる。この保護を使う場合、.NET は適切な設定(鍵の管理やトークンの管理など)の責任を利用者に負わせる。
- 1.25 以降、Go の開発者は組み込みの CrossOriginProtection 型に依拠できる。これは Fetch Metadata に基づく CSRF 防御(
Sec-Fetch-Siteと関連ヘッダの検証を含む)を標準ライブラリ内で直接実装する。
トークンに基づく緩和
シンクロナイザトークンパターンは、CSRF を緩和する最も一般的で推奨される方法の一つである。
シンクロナイザトークンパターン
CSRF のトークンはサーバー側で生成し、利用者のセッションごとに一度、あるいはリクエストごとに生成するべきである。 盗まれたトークンを攻撃者が悪用できる時間の幅がリクエストごとのトークンでは最小になるので、それはセッションごとのトークンより安全である。 ただしリクエストごとのトークンは使いやすさの懸念を生じうる。
たとえばブラウザの「戻る」ボタンの機能は、リクエストごとのトークンによって妨げられうる。 前のページがもはや有効でないトークンを含みうるからである。 その場合、前のページとのやりとりはサーバー側で CSRF の誤検知の安全事象を生む。 セッションごとのトークンの実装では、最初のトークンの生成後、その値がセッションに保存され、セッションが失効するまでその後のすべてのリクエストで使われる。
クライアントがリクエストを発するとき、サーバー側の構成要素はそのリクエスト内のトークンの存在と正当性を検証し、それを利用者セッション内のトークンと比較しなければならない。 リクエスト内にトークンが見つからない場合、あるいは与えられた値が利用者セッション内の値と一致しない場合、そのリクエストは拒否するべきである。 CSRF 攻撃が進行している可能性のある事象として記録するなど、追加の措置も検討するべきである。
CSRF のトークンは次を満たすべきである。
- 利用者セッションごとに一意である。
- 秘密である。
- 予測できない(安全な方法で生成された大きなランダム値である)。
CSRF のトークンがなければ攻撃者はバックエンドのサーバーへ正当なリクエストを作れないので、CSRF のトークンは CSRF を防ぐ。
シンクロナイザパターンにおける CSRF トークンの送信
CSRF のトークンは、HTML や JSON の応答のように応答のペイロードの一部としてクライアントへ送れる。 そしてフォーム送信の隠し項目として、あるいは AJAX のリクエストで独自ヘッダの値や JSON のペイロードの一部として、サーバーへ送り返せる。 シンクロナイザパターンでは、CSRF のトークンを Cookie で送るべきではない。 CSRF のトークンは、サーバーのログや URL に漏れてはならない。 GET のリクエストは、ブラウザの履歴、ログファイル、HTTP リクエストの最初の行を記録するネットワークの道具、そして保護対象のサイトが外部のサイトへリンクしている場合の Referer ヘッダなど、複数の箇所で CSRF のトークンを漏らしうる。
たとえば次のようになる。
<form action="/transfer.do" method="post">
<input type="hidden" name="CSRFToken" value="OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZMGYwMGEwOA==">
[...]
</form>
独自ヘッダを伴うリクエストは自動的に同一生成元ポリシーの対象になるので、隠しフォーム項目に CSRF のトークンを加えるより、JavaScript で独自の HTTP リクエストヘッダに CSRF のトークンを入れるほうが安全である。
代替。二重送信 Cookie パターンを使う
サーバー側で CSRF トークンの状態を保つことが問題になる場合、二重送信 Cookie パターンとして知られる代替の手法を使える。 この手法は実装が容易で、状態を持たない。 実装の方法はいくつかあり、素朴なパターンが最もよく使われる変種である。
署名付き二重送信 Cookie(推奨)
二重送信 Cookie パターンの最も安全な実装は署名付き二重送信 Cookie であり、トークンを利用者の認証済みセッション(セッション ID など)に明示的に結びつける。 セッションへの結びつけなしに単にトークンへ署名するだけでは保護は最小限であり、Cookie の注入攻撃に脆弱なままである。 CSRF のトークンは常にセッション固有のデータへ明示的に結びつける。
トークンが機微な情報(セッション ID やクレームなど)を含む場合は、常にサーバー側の秘密鍵を用いた HMAC(Hash-based Message Authentication)を使う。 これは完全性を確保しつつトークンの偽造を防ぐ。 HMAC は各種の暗号上の攻撃から守るので、あらゆる場合において単純なハッシュより望ましい。 トークンの内容の機密性が必要な場面では、代わりに認証付き暗号を使う。
HMAC の CSRF トークンを使う
(セッションに依存する利用者の値を含む)HMAC の CSRF トークンを生成するには、システムが次を備えている必要がある。
- ログインのセッションごとに変わる、セッションに依存する値。この値は、利用者の認証済みセッションの全体においてのみ有効であるべきである。利用者のメールアドレスや ID のような静的な値は安全でないので使うことを避ける(1、2、3)。リクエストごとのように CSRF のトークンを頻繁に更新することは、実質的な安全性を加えると想定する誤解であり、実際には利用者体験を損なうことに注意する(1)。たとえばセッションに依存する値として、次のいずれか、あるいはその組み合わせを選べる。
- 秘密の暗号鍵。素朴な実装におけるランダム値と混同しない。この値は HMAC のハッシュの生成に使う。この鍵は理想的には暗号によるデータ保存で述べたように保存する。
- 衝突を避けるためのランダム値。同じ秒内の連続した呼び出しが同じハッシュを生まないようにするため、(望ましくは暗号論的に)ランダムな値を生成する(1)。
失効のためにタイムスタンプを CSRF トークンに含めるべきか
CSRF トークンの失効時刻を指定する値としてタイムスタンプを含めるのは、よくある誤解である。 CSRF のトークンはアクセストークンではない。 それはセッションの情報を用いて、セッションを通じてリクエストの真正性を検証するために使われる。 新しいセッションが新しいトークンを生成するべきである(1)。
HMAC の CSRF トークンを実装する擬似コード
上で述べた実装の手順を示す擬似コードの例を示す。
// Gather the values
secret = getSecretSecurely("CSRF_SECRET") // HMAC secret key
sessionID = session.sessionID // Current authenticated user session
randomValue = cryptographic.randomValue(64) // Cryptographic random value
// Create the CSRF Token
message = sessionID.length + "!" + sessionID + "!" + randomValue.length + "!" + randomValue.toHex() // HMAC message payload
hmac = hmac("SHA256", secret, message) // Generate the HMAC hash
// Add the `randomValue` to the HMAC hash to create the final CSRF token.
// Avoid using the `message` because it contains the sessionID in plain text,
// which the server already stores separately.
csrfToken = hmac.toHex() + "." + randomValue.toHex()
// Store the CSRF Token in a cookie
response.setCookie("csrf_token=" + csrfToken + "; Secure") // Set Cookie without HttpOnly flag
クライアントから送り返された CSRF トークンの検証を示す擬似コードの例を示す。
// Get the CSRF token from the request
csrfToken = request.getParameter("csrf_token") // From header or form field (NOT cookie)
// Split the token to get the randomValue
const tokenParts = csrfToken.split(".");
const hmacFromRequest = tokenParts[0];
const randomValue = tokenParts[1];
// Recreate the HMAC with the current session and the randomValue from the request
secret = getSecretSecurely("CSRF_SECRET") // HMAC secret key
sessionID = session.sessionID // Current authenticated user session
message = sessionID.length + "!" + sessionID + "!" + randomValue.length + "!" + randomValue
// Generate the expected HMAC
expectedHmac = hmac("SHA256", secret, message)
// Compare the HMAC from the request with the expected HMAC
if (!constantTimeEquals(hmacFromRequest, expectedHmac)) {
// HMAC validation failed, reject the request
response.sendError(403, "Invalid CSRF token")
logError("Invalid CSRF token", hmacFromRequest, expectedHmac)
return
}
// CSRF validation passed, continue processing the request
// ...
注記:タイミング攻撃を防ぐため、HMAC の比較には constantTimeEquals 関数を使うべきである。
この関数は、何文字が一致するかに関わらず一定時間で二つの文字列を比較する。
素朴な二重送信 Cookie パターン(非推奨)
警告 素朴な二重送信 Cookie パターンは、標的のドメインに Cookie を書ける攻撃者(脆弱な兄弟サブドメイン、DNS の乗っ取り、
__Host-でない Cookie に対する平文 HTTP での Cookie 注入などによる)に回避されうる。新しいコードでは上記の署名付き二重送信 Cookieのパターンを使う。素朴なパターンは参照のためにのみ記載している。
素朴な二重送信 Cookie の方法は、拡張しやすく実装の容易な手法であり、暗号的に強固なランダム値を Cookie としてもリクエストのパラメータとしても使う(利用者の認証前でも)。 そしてサーバーが Cookie の値とリクエストの値が一致するかを検証する。
サイトは、利用者からのすべての取引のリクエストが、このランダム値を独自のリクエストヘッダまたはフォームのパラメータとしてのみ含むことを要求しなければならない。 Cookie による検証は安全でない。
なぜか。 ブラウザはサイトをまたぐリクエストで Cookie を自動的に送る。 攻撃者はこれを自動的に引き起こせる。 安全性には、利用者の意図を証明する明示的なクライアントからの送信(ヘッダやパラメータ)が必要である。
サーバー側で値が一致すればサーバーは正当なリクエストとして受け付け、一致しなければ拒否する。
攻撃者はサイトをまたぐリクエストの最中に Cookie の値へアクセスできないので、一致する値を隠しフォームの値やリクエストのパラメータやヘッダに含められない。
素朴な二重送信 Cookie の方法は単純で拡張しやすいが、Cookie の注入攻撃に脆弱なままである。 とくに攻撃者がサブドメインやネットワークの環境を制御でき、Cookie を仕込んだり上書きしたりできる場合である。 たとえば攻撃者が制御するサブドメイン(DNS の乗っ取りなどによる)から一致する Cookie を注入し、正当なリクエストのトークンを偽造できる。 これらの脆弱性の詳細はこの資料にある。 したがってこれらの脅威を緩和するため、常にセッションに結びついた HMAC のトークンを用いる署名付き二重送信 Cookie のパターンを優先する。
Fetch Metadata のヘッダ
Fetch Metadata のリクエストヘッダは、その HTTP リクエストがどの文脈から発せられたのかについての追加の情報を与える。
サーバーはこれらのヘッダ、とくに Sec-Fetch-Site を、明らかにサイトをまたぐリクエストを遮断する軽量で信頼できる方法として使える。
詳細は Fetch Metadata の仕様を参照する。
一部の古いブラウザは Sec-Fetch-* のヘッダを送らないので、Fetch Metadata の実装では標準のヘッダによる生成元の検証への代替が必須の要件である。
Sec-Fetch-* は 2023 年 3 月以降、主要なすべてのブラウザで対応されている。
Fetch Metadata のリクエストヘッダは次のものである。
Sec-Fetch-Site。CSRF 保護における主要な信号である。リクエストの発起元のオリジンと対象のオリジンの関係を示し、same-origin、same-site、cross-site、noneのいずれかをとる。Sec-Fetch-Mode、Sec-Fetch-Dest、Sec-Fetch-User。リクエストの文脈(リクエストのモード、宛先の種類、利用者の遷移によって引き起こされたかなど)についての情報を与える追加のヘッダである。さらに詳しくは MDN の解説にある。
上記のヘッダのいずれかが仕様に列挙されていない値を含む場合、前方互換性のためにサーバーはそのヘッダを無視するべきである。
使いやすさ
シンクロナイザトークンや二重送信のパターンは、クライアントとサーバーの追加の連携を要し、正しく実装するのが難しい。
それに対して Fetch Metadata の確認はずっと直接的である。
通常はサーバー側の少量のロジック(Sec-Fetch-Site を検査し、任意で Sec-Fetch-Mode や Sec-Fetch-Dest で精緻化する)のみを要し、クライアントの変更を要さない。
その単純さは複雑さを減らし、多くのアプリケーションにとってこの方法を魅力的にする。
ブラウザの互換性
Fetch Metadata のリクエストヘッダは、デスクトップとモバイルの両方の現代のすべてのブラウザ(Chrome、Edge、Firefox、Safari 16.4 以降、iOS と Android の WebView でも)で対応されており、世界の 98% 超を網羅する。 互換性の詳細はブラウザ対応の表を参照する。
Sec-Fetch-* に対応しない古い、あるいは組み込みのブラウザという稀な場合には、標準のヘッダによる生成元の検証への代替で必要な網羅を得られる。
プロジェクトとしてそれが許容できるなら、利用者にブラウザの更新を促すことを検討する。
古く安全でない可能性のあるバージョンで動いているからである。
サーバー側で Fetch Metadata のヘッダをどう扱うか
Sec-Fetch-Site は、CSRF に類する生成元をまたぐリクエストを遮断するうえで最も有用な Fetch Metadata のヘッダであり、Fetch Metadata に基づく方針における主要な信号にするべきである。
他の Fetch Metadata のヘッダ(Sec-Fetch-Mode、Sec-Fetch-Dest、Sec-Fetch-User)は、方針をアプリケーションの必要に合わせて精緻化したり調整したりするために使う(たとえばトップレベルの遷移のリクエストを許す、リソースのエンドポイントに特定の Dest の値を許すなど)。
方針(概要)
Sec-Fetch-Siteが存在する場合1.1. 状態を変える動作については cross-site を信頼できないものとして扱う。既定では、
Sec-Fetch-Site: cross-siteのときに安全でないメソッド(POST、PUT、PATCH、DELETE)を拒否する。const SAFE_METHODS = new Set(['GET','HEAD','OPTIONS']); const site = req.get('Sec-Fetch-Site'); // e.g. 'cross-site','same-site','same-origin','none' if (site === 'cross-site' && !SAFE_METHODS.has(req.method)) { return false; // forbid this request }1.2. アプリケーションが状態を変える動作に安全な HTTP メソッド(GET、HEAD、OPTIONS)を用いている場合は、それを方針に明示的に反映するべきである。たとえばそれらのエンドポイントへのリクエストに Fetch Metadata のヘッダの確認を要求する。これは次のような方針の規則で強制できる。
const SAFE_METHODS = new Set(['GET','HEAD','OPTIONS']); const SENSITIVE_ENDPOINTS = new Set([ '/user/profile', '/account/details', ]); const site = req.get('Sec-Fetch-Site'); const path = req.path; // Block if cross-site + unsafe method OR cross-site + sensitive endpoint if (site === 'cross-site' && (!SAFE_METHODS.has(req.method) || SENSITIVE_ENDPOINTS.has(path))) { return false; // forbid this request }1.3.
same-originを許可する。same-siteを許可するのは、脅威モデルが兄弟のサブドメインを信頼する場合に限る。そうでなければsame-siteは慎重に扱う(たとえば追加の検証を要求する)。const trustSameSite = false; // set true only if you trust sibling subdomains if (site === 'same-origin') { return true; } else if (site === 'same-site') { // handle same-site separately so the subcondition is clearly scoped to same-site if (!trustSameSite && !SAFE_METHODS.has(req.method)) { return false; // treat same-site as untrusted for state-changing methods } return true; }1.4. 適切な場合には、利用者が起こすトップレベルの遷移(ブックマーク、手入力の URL、明示的なフォーム送信)に対して none を許可する。
Sec-Fetch-*のヘッダが存在しない場合。リスクと互換性の要件に基づいて代替を選ぶ。 2.1. 安全側に倒す(機微なエンドポイントに推奨)。不在を不明として扱い、リクエストを遮断する。 2.2. 開く側に倒す(互換性を優先)。標準のヘッダによる生成元の検証、CSRF のトークン、あるいは追加の検証の要求へ落とす。追加の選択肢
3.1. 自分のサイトが他のサイトからリンクされ続けられる状態にするには、単純な(HTTP GET の)トップレベルの遷移を許可しなければならない。
if (req.get('Sec-Fetch-Mode') === 'navigate' && req.method === 'GET' && req.get('Sec-Fetch-Dest') !== 'object' && req.get('Sec-Fetch-Dest') !== 'embed') { return true; // Allow this request }3.2. 明示的に生成元をまたぐ流れを許可リストに入れる。一部のエンドポイントが意図的に生成元をまたぐリクエストを受け付ける場合(CORS の JSON API、サードパーティとの統合、Webhook など)は、それらを大域的な
Sec-Fetchの拒否の方針から明示的に除外し、適切な CORS の設定、認証、ログによって保護する。
要件
- アプリケーションが信頼できる URL で提供されていなければならない。Fetch Metadata のリクエストヘッダは信頼できる可能性のある URLに対してのみ送られる。実際には
https、wss、file、localhost(127.0.0.0/8と::1/128を含む)が含まれる。全体の詳細は W3C の Secure Contexts の仕様を参照する。 - アプリケーション全体で HTTPS を強制しなければならない。これにより Fetch Metadata のヘッダが一貫して含まれる。HTTP Strict Transport Security(HSTS)を有効にすると、すべての HTTP のリクエストが自動的に HTTPS へ昇格され、これを達成する助けになる。
- 安全な HTTP メソッドを、状態を変えるリクエストに使うべきではない。
懸念
- 事前描画や事前取得、その他の投機的な遷移は、最終的な遷移と一致しない
Sec-Fetch-*の値を送りうる。またブラウザが開始する流れ(PaymentRequest など)は、予測できる Fetch Metadata のヘッダを持たないリクエストを生成しうる。これらの挙動はなお整備の途上にあり、ヘッダの伝播はすべての遷移の種類で完全に安定しているとはいえない。 - 中継装置(プロキシ、ゲートウェイ、負荷分散装置)は
OriginやSec-*のヘッダを削除または変更しうる。プライバシーのフィルタ、ネットワークの最適化、あるいは単なる設定の誤りによるものであり、これは Fetch Metadata に基づく保護を壊しうる。この種のヘッダの除去は問題であるが、よくあることである。
導入と検証の推奨
- キャッシュが応答を適切に扱う状態にするため、適切な
Varyヘッダを含める。たとえばVary: Sec-Fetch-Site, Originとする。さらに詳しくは Fetch Metadata の仕様を参照する。Varyヘッダは CSRF の防御にはまったく影響しないことに注意する。これは応答のヘッダなので、サーバーが CSRF 保護に基づいて許可か拒否の判断を下したあとに適用される。その目的は防御ではなく運用上のものである。- サーバーが HTTP のヘッダ(
Sec-Fetch-Site、Originなど)に基づいて異なる応答を返す場合、キャッシュはそれらのヘッダによって分ける必要がある。そうしなければ、CDN やプロキシが別の文脈のために生成された応答を再利用し、挙動の破綻やキャッシュ汚染の場面につながりうる。適切なVaryヘッダを加えれば、キャッシュはこれらの応答を分けて保つ。
- 「記録のみ」のモードから始める。遮断されることになるリクエストを記録し、強制の前に誤検知を確認する。これは許可リストに入れる必要のある正当な流れを見つける最も安全な方法である。
- ユーザーエージェントの網羅を監視する。どのユーザーエージェントが
Sec-Fetch-*を含み、どれが含まないかを追跡し、代替のロジックがヘッダ不在の場合を網羅する状態にする。より厳しい方針を強制する時期の判断には計測値を使う。 - 例外を文書化する。生成元をまたぐアクセスを許可リストに入れたエンドポイントの明示的な一覧を保つ。
単純なリクエストを許さない
データの送信に <form> タグが使われる場合、ブラウザが「事前検証の対象」と指定しない「単純な」リクエストが送られる。
これらの「単純な」リクエストは CSRF のリスクを持ち込む。
ブラウザがそれを任意のオリジンへ送ることを許すからである。
クライアントのどこかでデータの送信に <form> タグを使っている場合は、トークンのようなこの文書で述べる別の方法で保護する必要がある。
注意点 ブラウザの不具合が独自の HTTP ヘッダを許してしまう、あるいは単純でないコンテンツ種別に対して事前検証を強制しない場合、安全性が損なわれうる。起こりにくいとはいえ、脅威モデルではこれを考慮するのが賢明である。CSRF のトークンを実装すれば防御の層が加わり、開発者はアプリケーションの安全性をより制御できる。
単純なコンテンツ種別を許さない
リクエストが単純とみなされるには、コンテンツ種別が application/x-www-form-urlencoded、multipart/form-data、text/plain のいずれかでなければならない。
現代の Web アプリケーションの多くは JSON の API を使うので当然 CORS を要するが、text/plain を受け付けてしまうと CSRF に脆弱になる。
したがって単純な緩和策は、サーバーや API がこれらの単純なコンテンツ種別を許さないことである。
AJAX や API に独自のリクエストヘッダを使う
シンクロナイザトークンと二重送信 Cookie はどちらもフォームのデータの偽造を防ぐために使われるが、実装が厄介で使いやすさを損ないうる。
現代の Web アプリケーションの多くは、データの送信に <form> タグを使わない。
とくに AJAX や API のエンドポイントに適した、利用者に親切な防御は独自のリクエストヘッダの使用である。
この方法にトークンは不要である。
このパターンでは、クライアントが CSRF 保護を要するリクエストに独自のヘッダを付ける。 既存のヘッダと衝突しないかぎり、そのヘッダは任意のキーと値の組でよい。
X-CSRF-Token: RANDOM-TOKEN-VALUE
多くの一般的なフレームワークは、CSRF 保護に標準化されたヘッダ名を使う。
X-CSRF-Token。Ruby on Rails、Laravel、DjangoX-XSRF-Token。AngularJSCSRF-Token。Express.js(csurf のミドルウェア)X-CSRFToken。Django
任意のヘッダ名でも機能するが、これらの標準的な名前のいずれかを使うと、既存の道具や開発者の期待との相性が良くなる。
リクエストを処理する際、API はこのヘッダの存在を確認する。 ヘッダが存在しなければ、バックエンドは偽造の可能性があるものとしてそのリクエストを拒否する。 この方法にはいくつもの利点がある。
- UI の変更が不要である
- トークンを追跡するサーバーの状態を持ち込まない
この防御は、宛先のサーバーとの CORS の適合を検証するために OPTIONS のリクエストを送る CORS の事前検証の仕組みに依拠する。
現代のすべてのブラウザは、独自ヘッダを伴うリクエストを「事前検証の対象」と指定する。
API がその独自ヘッダの存在を検証できれば、そのリクエストがブラウザから来たものであれば事前検証を通っていたことがわかる。
独自ヘッダと CORS
Cookie は既定では生成元をまたぐリクエスト(CORS)で設定されない。
API で Cookie を有効にするには Access-Control-Allow-Credentials=true を設定する。
資格情報が許される場合、ブラウザは Access-Control-Allow-Origin=* を含む応答をすべて拒否する。
CORS のリクエストを許しつつ CSRF から守るには、Access-Control-Allow-Origin ヘッダを通じて、確実に自分が管理する少数の選ばれたオリジンのみをサーバーが許す状態にする必要がある。
許可されたドメインからの生成元をまたぐリクエストは、独自ヘッダを設定できる。
たとえば http://www.yoursite.com と http://mobile.yoursite.com からの Cookie を伴う CORS を許すようバックエンドを設定すると、ありうる事前検証の応答は次の二つだけになる。
Access-Control-Allow-Origin=http://mobile.yoursite.com
Access-Control-Allow-Credentials=true
あるいは次のようになる。
Access-Control-Allow-Origin=http://www.yoursite.com
Access-Control-Allow-Credentials=true
より安全性の低い設定は、正規表現を使って自サイトのすべてのサブドメインからの CORS を許すようバックエンドのサーバーを設定することである。 攻撃者がサブドメインを乗っ取れた場合(クラウドのサービスでは珍しくない)、その CORS の設定は攻撃者が同一生成元ポリシーを回避し、独自ヘッダ付きのリクエストを偽造することを許してしまう。
クライアント側の CSRF 攻撃への対処(重要)
クライアント側の CSRF は CSRF 攻撃の新しい変種であり、攻撃者はプログラムの入力パラメータを操作して、クライアント側の JavaScript のコードを騙し、脆弱な標的のサイトへ偽造した HTTP リクエストを送らせる。 クライアント側の CSRF は、JavaScript のプログラムが URL のような攻撃者が制御できる入力を、非同期の HTTP リクエストの生成に使うときに生じる。
注記:これらの CSRF の変種はとくに重要である。トークンに基づく緩和や SameSite の Cookie のような一般的な CSRF 対策の一部を回避できるからである。 たとえばシンクロナイザトークンや独自の HTTP リクエストヘッダが使われている場合、JavaScript のプログラムはそれらを非同期のリクエストに含めてしまう。 またブラウザは、JavaScript のプログラムが開始した同一サイトのリクエストの文脈で Cookie を含めるので、SameSite の Cookie の方針を回り込む。
クライアント側の CSRF と古典的な CSRF:古典的な CSRF のモデルでは、サーバー側のプログラムが最も脆弱な構成要素である。 受け取った認証済みのリクエストが意図的に行われたのかを区別できないからであり、これは混乱した代理人の問題として知られる。 クライアント側の CSRF のモデルでは、最も脆弱な構成要素はクライアント側の JavaScript のプログラムである。 攻撃者はリクエストのエンドポイントやそのパラメータを操作して、任意の非同期のリクエストを生成させられるからである。 クライアント側の CSRF は入力検証の問題によって生じ、混乱した代理人の欠陥を再導入する。 すなわちサーバー側は再び、そのリクエストが意図的に行われたのかを区別できなくなる。
クライアント側の CSRF 脆弱性についてさらに詳しくは、この論文の 2 節と 5 節、SameSite wiki の CSRF の章、そして Meta Bug Bounty Program によるこの記事を参照する。
クライアント側の CSRF の例
次のコードの断片は、クライアント側の CSRF 脆弱性の単純な例を示す。
<script type="text/javascript">
const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const ajaxLoad = () => {
// process the URL hash fragment
const hashFragment = window.location.hash.slice(1);
// hash fragment should be of the format: /^(get|post);(.*)$/
// e.g., https://site.com/index/#post;/profile
if (hashFragment.length > 0 && hashFragment.includes(';')) {
const params = hashFragment.match(/^(get|post);(.*)$/);
if (params && params.length) {
const requestMethod = params[1];
const requestEndpoint = params[3];
fetch(requestEndpoint, {
method: requestMethod,
headers: {
'X-CSRF-Token': csrf_token,
// [...]
},
// [...]
})
.then(response => { /* [...] */ })
.catch(error => console.error('Request failed:', error));
}
}
};
// trigger the async request on page load - better practice is to use event listeners
window.addEventListener('DOMContentLoaded', ajaxLoad);
</script>
脆弱性:この断片では、ページの読み込み時にプログラムが ajaxLoad() 関数を呼び、それがページの各要素の読み込みを担う。
この関数は URL のハッシュフラグメントの値を読み(4 行目)、そこから二つの情報(リクエストのメソッドとエンドポイント)を取り出して非同期の HTTP リクエストを生成する(11 行目から 13 行目)。
脆弱性は 15 行目から 22 行目で生じる。
JavaScript のプログラムが、非同期の HTTP リクエストのサーバー側のエンドポイント(15 行目)とリクエストのメソッドを得るために URL のフラグメントを使っているからである。
しかしどちらの入力も攻撃者が制御できるので、攻撃者は好きな値を選び、攻撃のペイロードを含む悪意ある URL を作れる。
攻撃:通常、攻撃者は(標的型のフィッシングメールなどによって)悪意ある URL を被害者と共有する。
その URL は正直で評判の良い(しかし脆弱な)サイトのものに見えるので、利用者はしばしばそれをクリックする。
あるいは攻撃者は攻撃用のページを作ってブラウザの API(window.open() など)を濫用し、標的のページの脆弱な JavaScript を騙して HTTP リクエストを送らせられる。
これは古典的な CSRF 攻撃の攻撃モデルによく似ている。
クライアント側の CSRF のさらなる例は、Meta Bug Bounty Program によるこの記事と、この USENIX Security の論文を参照する。
クライアント側の CSRF の緩和手法
独立したリクエスト:非同期のリクエストが、URL、ウィンドウ名、文書の referrer、postMessage のような攻撃者が制御できる入力から生成できない状態にすれば、クライアント側の CSRF を防げる。
入力の検証:文脈と機能によっては、入力とリクエストのパラメータのあいだの完全な分離を達成できないこともある。 その場合は入力検証の確認を実装しなければならない。 この確認は、リクエストのパラメータの値の形式と選択を厳密に評価し、それが状態を変えない操作にのみ使えるかを判断するべきである(GET のリクエストと、あらかじめ定めた接頭辞で始まるエンドポイントのみを許すなど)。
あらかじめ定めたリクエストのデータ:もう一つの緩和手法は、あらかじめ定めた安全なリクエストのデータの一覧を JavaScript のコードに保持することである(再送しても安全なエンドポイント、リクエストのメソッド、その他のパラメータの組み合わせなど)。 そのうえでプログラムは URL のフラグメント内の切り替えのパラメータを使い、各 JavaScript の関数がその一覧のどの項目を使うかを決められる。
多層防御の手法
SameSite(Cookie の属性)
SameSite は(HttpOnly や Secure などと似た)Cookie の属性であり、CSRF 攻撃の緩和を目的とする。
これは RFC6265bis で定義されている。
この属性は、サイトをまたぐリクエストとともに Cookie を送るかどうかをブラウザが判断する助けになる。
この属性がとりうる値は Lax、Strict、None である。
Strict の値は、通常のリンクをたどる場合でも、サイトをまたぐあらゆる閲覧の文脈において、ブラウザが標的のサイトへ Cookie を送ることを防ぐ。
たとえば GitHub のようなサイトが Strict の値を使っている場合、ログイン済みの GitHub の利用者が、企業の掲示板やメールに投稿された非公開の GitHub プロジェクトへのリンクをたどろうとしても、GitHub がセッションの Cookie を受け取らないため、そのプロジェクトへアクセスできない。
銀行のサイトは取引のページが外部のサイトからリンクされることを許さないので、Strict のフラグは銀行に最も適するだろう。
外部のリンクから来た利用者のログイン済みセッションを維持したいサイトには、SameSite の既定値である Lax が安全性と使いやすさの妥当な釣り合いを与える。
上の GitHub の場面で代わりに Lax の値を使えば、外部のサイトからの通常のリンクをたどるときはセッションの Cookie が許され、POST のような CSRF を受けやすいリクエストのメソッドでは遮断される。
Lax のモードで許されるサイトをまたぐリクエストは、トップレベルの遷移であり、安全な HTTP メソッドを使うものだけである。
SameSite の値についてさらに詳しくは、RFC のこの節を参照する。
この属性を使う Cookie の例を示す。
Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict
Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax
現代のデスクトップとモバイルのすべてのブラウザが SameSite 属性に対応している。
主な例外は、Opera Mini(全バージョン)、Android の UC Browser、古いモバイルのブラウザ(iOS Safari 13.2 未満、Android Browser 97 未満)を含むレガシーなブラウザである。
これを実装したブラウザの追跡と属性の使われ方はこのサービスを参照する。
Chrome は 2020 年に SameSite=Lax を既定の挙動として実装し、Firefox と Edge もそれに続いた。
加えて、SameSite=None が付けられた Cookie には Secure のフラグが必須である。
SameSite の限界
SameSite は多層防御の対策として有用であるが、ほとんどの環境で適切な CSRF 防御を置き換えるものではない。
それが実際にどれだけの保護を与えるかを考えるとき、次を既知の穴として扱う。
Laxは安全でないメソッドのみを遮断する。 既定のLaxの挙動は、安全なメソッド(GET、HEAD、OPTIONS、TRACE)を使うトップレベルの遷移では Cookie をなお許す。アプリケーション内の状態を変える操作がGETのリクエストで到達できるなら、SameSite=Laxはそれを止めない。これは実際にSameSiteに基づく防御が失敗する最もよくある形である。すべてのGETのエンドポイントを見直し、いずれもサーバー側の状態を変えない状態にする。SameSiteはオリジンではなく登録可能ドメインを範囲とする。app.example.comに設定された Cookie は、SameSiteの値が何であれ、リクエストがanything.example.comから発せられた場合もなお「same-site」とみなされる。アプリケーションが、完全に管理できないコンテンツと登録可能ドメインを共有している場合(共有された親ドメイン上のマルチテナント SaaS、サブドメインで提供される利用者のコンテンツ、レガシーなサブドメイン、同じ親ドメインで動く買収した製品、サブドメイン上のサードパーティのサービスなど)、それらの兄弟ホストのいずれかにある脆弱性や悪意ある者が、ブラウザが same-site として扱うリクエストを発せられる。これはサブドメインの乗っ取りの影響も増幅する。放置されたサブドメインを取得した攻撃者は、SameSiteで保護された Cookie が付いてくるリクエストを発せられる。- トップレベルの遷移とウィンドウを開く手口。 被害者にトップレベルの遷移を行わせるか、自分のサイトを対象とする新しいウィンドウを開かせられる攻撃者(事前描画のヒント、
window.open、作り込んだリンクのクリックを通じたものを含む)は、ブラウザが same-site として扱うリクエストを生成できる。SameSite=Strictはこれらの多くを遮断するが、アプリケーションへのサイトをまたぐ正当なリンクを壊す代償を伴う。 - ブラウザの網羅は普遍的でない。 現在の主流のブラウザは既定で
SameSite=Laxを強制するが、古いブラウザ、組み込みのブラウザ、主流でないクライアントの利用者は、SameSiteの値が設定されていないかのように振る舞う Cookie を受け取りうる。すべての通信がこの保護を享受していると想定しない。 - クライアント側の CSRF には効かない。
SameSiteはサイトをまたぐリクエストに作用する。悪意ある入力によって、自分のアプリケーション内の同一生成元の JavaScript が状態を変えるリクエストを発する、クライアント側の CSRF(前述の節を参照)からは守らない。
SameSite だけで十分になりうる場合
限られた環境では、次のすべてが成り立つかぎり、SameSite だけで妥当な CSRF 防御を与えうる。
- アプリケーションが、完全に管理できないホスト、サブドメイン、サービスと登録可能ドメインを共有していない。
- アプリケーション内のどの
GET(あるいは他の安全なメソッドの)エンドポイントも、状態を変える動作を行わない。すべての状態の変更がPOST、PUT、PATCH、DELETEを要する。 - セッションの Cookie が
SameSite=Strictで設定されている、あるいはSameSite=Laxを__Host-の接頭辞と、すべてのGETのハンドラの厳密な監査とあわせて使っている。 - 状態を変えるエンドポイントに対して、多層防御として Origin または Referer の検証(後述)が施されている。
SameSiteを強制しないブラウザの利用者を除外することを受け入れられる、あるいはそれらの利用者が負う残余のリスクを受け入れられる。
上記のすべてを満たさないアプリケーションでは、SameSite は多層防御の一層として扱い、それだけに依存するのではなく CSRF のトークンや二重送信のパターンと組み合わせるべきである。
標準のヘッダによる生成元の検証
この緩和の方法には二つの段階があり、どちらも HTTP リクエストのヘッダの値を調べる。
- リクエストが来た生成元(送信元のオリジン)を判定する。
OriginまたはRefererのヘッダで行える。 - リクエストが向かう先の生成元(対象のオリジン)を判定する。
サーバー側で、両者が一致するかを検証する。 一致すればそのリクエストを正当なもの(同一生成元のリクエスト)として受け付け、一致しなければ破棄する(ドメインをまたいで発せられたことを意味する)。 これらのヘッダを信頼できる根拠は、それらが禁止ヘッダの一覧に入り、プログラムから変更できないことにある。 つまりブラウザだけがそれを設定できる。
送信元のオリジンの特定(Origin ヘッダと Referer ヘッダによる)
Origin ヘッダを確認する
Origin ヘッダが存在する場合、その値が対象のオリジンと一致することを検証する。
Referer と違い、Origin ヘッダは HTTPS の URL から発せられた HTTP のリクエストに存在する。
Origin ヘッダがない場合に Referer ヘッダを確認する
Origin ヘッダが存在しない場合、Referer ヘッダのホスト名が対象のオリジンと一致することを検証する。
この CSRF 緩和の方法は、シンクロナイザトークンの追跡に必要なセッションの状態を確立する前に行われるリクエストのような、認証されていないリクエストにもよく使われる。
いずれの場合も、対象のオリジンの確認が強固である状態にする。
たとえばサイトが example.org であれば、example.org.attacker.com が生成元の確認を通らない状態にする(オリジンの後の末尾の / まで含めて照合し、オリジン全体と照合していることを確かにする)。
これらのヘッダがどちらも存在しない場合、リクエストを受け付けるか遮断するかを選べる。 遮断を推奨する。 あるいはそうした事例をすべて記録し、その用途と挙動を監視し、十分な確信を得たあとに遮断を始めることもできる。
対象のオリジンの特定
一般に、対象のオリジンの判定は必ずしも容易ではない。 リクエストの URL から対象のオリジン(そのホスト名とポート番号)を単純に取り出せるとはかぎらない。 アプリケーションのサーバーが一つ以上のプロキシの背後にあることが多いからである。 これは元の URL が、アプリケーションのサーバーが実際に受け取る URL と異なりうることを意味する。 ただしアプリケーションのサーバーが利用者から直接アクセスされるなら、URL のオリジンを使えば問題ない。
プロキシの背後にある場合、検討すべき選択肢はいくつもある。
- アプリケーションに自身の対象のオリジンを単に知らせる設定にする。自分のアプリケーションなのだから、その対象のオリジンを調べ、その値をサーバーの設定項目に記載できる。サーバー側で定義される信頼できる値なので、これは最も安全な方法である。ただしアプリケーションが多くの場所(開発、テスト、QA、本番、そして複数の本番のインスタンスなど)に配備されている場合、保守が問題になりうる。それぞれについて正しい値を設定するのは難しいかもしれないが、中央の設定で行い、各インスタンスがそこから値を取得できるようにできるなら、それでよい(注記:CSRF 防御の大部分がそれに依存するので、集約された設定の保管庫は安全に保守する)。
- Host ヘッダの値を使う。配備されたインスタンスごとに設定せずにアプリケーション自身が対象を見つけられるようにしたい場合は、
Host系のヘッダを使うことを推奨する。Hostヘッダはリクエストの対象のオリジンを含むことを意図している。ただしアプリケーションのサーバーがプロキシの背後にある場合、Hostヘッダの値はプロキシによって、プロキシの背後の URL の対象のオリジンへ変えられている可能性が高く、それは元の URL とは異なる。この変更されたHostヘッダのオリジンは、元のOriginやRefererのヘッダにある送信元のオリジンと一致しない。 - X-Forwarded-Host ヘッダの値を使う。プロキシが
Hostヘッダを変えてしまう可能性を避けるには、プロキシが受け取った元のHostヘッダの値を含むX-Forwarded-Hostという別のヘッダを使える。ほとんどのプロキシは元のHostヘッダの値をX-Forwarded-Hostヘッダで引き渡す。したがってX-Forwarded-Hostの値は、OriginやRefererのヘッダにある送信元のオリジンと比較すべき対象のオリジンの値である可能性が高い。
このヘッダの値を緩和に使う方法は、リクエストに Origin や Referer のヘッダが存在するときに適切に働く。
これらのヘッダは大半の場合に含まれるが、含まれない用途もいくつかある(その多くは利用者のプライバシーを守る、あるいはブラウザの生態系に合わせるという正当な理由による)。
X-Forwarded-Host が使えない場合
- 生成元をまたぐ 302 のリダイレクトをたどる場合、
Originはリダイレクト後のリクエストに含まれない。他のオリジンへ送るべきでない機微な情報とみなされうるからである。 Originが "null" に設定されるプライバシーの文脈がいくつかある。Originヘッダは生成元をまたぐすべてのリクエストに含まれるが、同一生成元のリクエストでは、ほとんどのブラウザで POST、DELETE、PUT にのみ含まれる。注記:理想的ではないが、多くの開発者が状態を変える操作に GET のリクエストを使っている。Refererヘッダも例外ではない。referrer のヘッダが省かれる用途も複数ある(1、2、3、4)。負荷分散装置、プロキシ、組み込みのネットワーク機器も、記録におけるプライバシーの理由で referrer のヘッダを除去することがよく知られている。
通常、上記の分類にあたる通信はわずかな割合(1% から 2%)であり、企業はこの通信を失いたくない。
この手法をより使いやすくするためにインターネット上で広く使われる技法の一つは、Origin や referrer が設定した許可ドメインの一覧に一致する場合、または null の値である場合にリクエストを受け付けることである(null の値は、上述のこれらのヘッダが送られない境界事例を網羅するためである)。
攻撃者はこれを悪用しうることに注意する。
それでも導入の労力が小さいため、多層防御の一手段としてこの技法を使うことが好まれている。
ホストの接頭辞を持つ Cookie で生成元を識別する
前述の SameSite と Secure の属性は、すでに設定された Cookie の送信を制限し、HttpOnly は設定された Cookie の読み取りを制限する。
一方で攻撃者は、他の方法で保護された Cookie を注入または上書きしようと試みうる(セッション固定攻撃を参照)。
CSRF のトークンを持つ Cookie に Cookie の接頭辞を使うと、この種の攻撃に対する保護も広がる。
Cookie が __Host- の接頭辞を持つ場合(Set-Cookie: __Host-token=RANDOM; path=/; Secure など)、その Cookie は次の性質を持つ。
- 別のサブドメインから書き込みや上書きができない。
Domain属性を持てない。/のパスを持たなければならない。Secureが付いていなければならない(暗号化されていない HTTP では送れない)。
__Host- の接頭辞に加えて、より弱い __Secure- の接頭辞もブラウザのベンダーによって対応されている。
これはドメインの上書きに関する制限を緩めるので、次のようになる。
Domain属性を持てる。- サブドメインから上書きできる。
/以外のPathを持てる。
認証済みの利用者が異なる(サブ)ドメインを訪れる必要がある場合、この緩い変種は「ドメインに固定された」__Host- の接頭辞の代替として使える。
それ以外のすべての場合には、SameSite 属性に加えて __Host- の接頭辞を使うことが推奨される。
Cookie の接頭辞は主要なすべてのブラウザで対応されている。
Cookie の接頭辞についてさらに詳しくは Mozilla Developer Network と IETF の草案を参照する。
利用者の操作に基づく CSRF 防御
ここで挙げた手法はいずれも利用者の操作を必要としないが、認可されていない操作(CSRF などによって偽造されたもの)を防ぐために、取引に利用者を関与させるほうが容易あるいは適切な場合もある。 正しく実装されれば強固な CSRF 防御として働きうる手法の例を挙げる。
- 再認証の仕組み
- 一度だけ使えるトークン
CAPTCHA は使わない。 それはボットに対する保護のために特に設計されたものである。 CAPTCHA の一部の実装では、別の利用者セッションから人間の操作や存在の証明を得ることが可能であり、いまも有効である。 これは CSRF の悪用をより複雑にはするが、それを防ぐわけではない。
これらは非常に強固な CSRF 防御であるが、利用者体験に大きな影響を与えうる。 したがって通常は、このチートシートで論じた他の防御とあわせて、安全性に関わる重要な操作(パスワードの変更、送金など)にのみ使われる。
ログインフォームにおける CSRF 脆弱性の可能性
ほとんどの開発者はログインフォームにおける CSRF 脆弱性を無視しがちである。 その段階では利用者が認証されていないので、ログインフォームには CSRF があてはまらないと想定するからである。 しかしこの想定は常に正しいとはいえない。 利用者が認証されていないログインフォームでも CSRF 脆弱性は生じうるが、その影響とリスクは異なる。
たとえば攻撃者が CSRF を使って、買い物サイトで攻撃者のアカウントを用いて標的の被害者に認証済みの身元を引き受けさせ、被害者がクレジットカードの情報を入力した場合、攻撃者は被害者の保存されたカードの情報で商品を購入できるかもしれない。 ログイン CSRF とその他のリスクについてさらに詳しくは、この論文の 3 節を参照する。
ログイン CSRF は、事前セッション(利用者が認証される前のセッション)を作り、ログインフォームにトークンを含めることで緩和できる。 トークンの生成には上述のいずれの手法も使える。 利用者が認証されたあと、事前セッションを本来のセッションへ移行させてはならないことを忘れない。 セッション固定攻撃を避けるため、そのセッションを破棄して新しいものを作るべきである。 この手法は Robust Defenses for Cross-Site Request Forgery の 4.1 節で述べられている。 ログイン CSRF は、前述のように AJAX のリクエストに独自のリクエストヘッダを含めることでも緩和できる。
参考。CSRF 保護を示す JEE のフィルタの例
次の JEE の Web フィルタは、このチートシートで述べた考え方の一部の参考例を与える。 これは次の状態を持たない緩和策を実装している(OWASP CSRFGuard は状態を持つ方法を扱う)。
- 標準のヘッダによる同一生成元の検証
- 二重送信 Cookie
- SameSite の Cookie 属性
注記:これは参考の例にすぎず完全ではない(たとえば Origin と Referer のヘッダの確認が成功したときに制御の流れを導く部分がなく、Referer ヘッダのポート、ホスト、プロトコルの水準の検証もない)。
開発者はこの参考例を土台に、完全な緩和策を構築することが推奨される。
また CSRF の確認が有効とみなされる前に、認証と認可の仕組みを実装するべきである。
完全なソースはこちらにあり、動作する実証を提供する。
JavaScript。AJAX のリクエストヘッダに CSRF トークンを自動的に含める
以下の JavaScript の指針は、既定で GET、HEAD、OPTIONS のメソッドを安全な操作とみなす。 したがってこれらのメソッドの AJAX の呼び出しには CSRF のトークンのヘッダを付ける必要がない。 ただしこれらのメソッドが状態を変える操作に使われている場合は、CSRF のトークンのヘッダも必要になる(ただしこれは悪い慣行であり、避けるべきである)。
POST、PUT、PATCH、DELETE のメソッドは状態を変えるので、リクエストに CSRF のトークンを付けるべきである。 以下の指針は、上述の状態を変えるメソッドのすべての AJAX リクエストに CSRF のトークンを自動的に含めるため、JavaScript のライブラリで上書きを作る方法を示す。
CSRF トークンの値を DOM に保持する
CSRF のトークンは、以下のように <meta> タグに含められる。
そのページのその後のすべての呼び出しは、この <meta> タグから CSRF のトークンを取り出せる。
JavaScript の変数や DOM 上のどこかに保持することもできる。
ただし CSRF のトークンを Cookie やブラウザのローカルストレージに保持することは推奨されない。
CSRF のトークンを <meta> タグとして含めるには、次のコードの断片を使える。
<meta name="csrf-token" content="{{ csrf_token() }}">
content 属性を埋める正確な構文は、その Web アプリケーションのバックエンドのプログラミング言語によって決まる。
既定を上書きして独自のヘッダを設定する
いくつもの JavaScript のライブラリは、既定の設定を上書きして、すべての AJAX リクエストにヘッダを自動的に加えられるようにしている。
XMLHttpRequest(素の JavaScript)
XMLHttpRequest の open() メソッドを上書きし、次に open() が呼ばれたときに X-CSRF-Token ヘッダを設定できる。
以下で定義する csrfSafeMethod() 関数は安全な HTTP メソッドを除外し、安全でない HTTP メソッドにのみヘッダを加える。
これは次のコードの断片のように行える。
<script type="text/javascript">
const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const csrfSafeMethod = (method) => {
// these HTTP methods do not require CSRF protection
return /^(GET|HEAD|OPTIONS)$/.test(method);
};
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(...args) {
const result = originalOpen.apply(this, args);
if (!csrfSafeMethod(args[0])) {
this.setRequestHeader('X-CSRF-Token', csrf_token);
}
return result;
};
</script>
現代のフレームワークにおける CSRF の防止
Angular、React、Vue のような現代のシングルページアプリケーション(SPA)のフレームワークは、通常 CSRF 攻撃の緩和に Cookie からヘッダへ渡すパターンに依拠する。 この方法は、ブラウザが生成元をまたぐリクエストに Cookie を自動的に付ける一方で、同一生成元で動く JavaScript だけがその値を読んで独自のヘッダを設定できるという事実を活用し、偽造されたリクエストの検出と遮断を可能にする。 Cookie からヘッダへ渡すパターンは次のように働く。
- サーバーが CSRF のトークンを生成する。利用者が認証するかアプリを読み込むと、サーバーが CSRF のトークンを Cookie に設定する(
XSRF-TOKENなど)。この Cookie は JavaScript からアクセスできる(すなわちHttpOnlyではない)ものであり、通常SameSite=LaxかStrictを持つ。 - クライアントがトークンを読む。SPA(多くは Angular の HttpClient や React・Vue の axios のようなライブラリを使う)が Cookie から CSRF のトークンを読む。
- クライアントがトークンを独自のヘッダに付ける。状態を変える各リクエスト(
POST、PUT、DELETEなど)について、クライアントはそのトークンを独自の HTTP ヘッダ(一般にX-XSRF-TOKENかX-CSRF-TOKEN)に設定する。 - サーバーがトークンを検証する。サーバーはヘッダのトークンが Cookie のものと一致するかを確認する。一致すればリクエストを受け付け、しなければ偽造の可能性があるものとして拒否する。
Angular はこのパターンを標準で提供し、HttpClient を通じて 2 と 3 の段階を自動的に扱う。 対して React や Vue のようなフレームワークでは、開発者がこのロジックを手作業で、あるいは axios のインターセプタのような補助のライブラリで実装する必要がある。 このパターンは、ブラウザが偽造されたリクエストに Cookie を含めたとしても、攻撃者が別のオリジンから一致する独自のヘッダを設定できないことを確かにする。
Angular
Angular の HttpClient は、XSRF 攻撃を防ぐために使われる Cookie からヘッダへ渡すパターンに対応する。
HTTP のリクエストを行うとき、インターセプタが Cookie(既定では XSRF-TOKEN)からトークンを読み、それを HTTP ヘッダ X-XSRF-TOKEN に設定する。
さらに詳しくは Angular の HttpClient XSRF/CSRF security の解説を参照する。
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withXsrfConfiguration({})),
provideRouter(routes, withComponentInputBinding()),
],
};
このコードの断片は Angular 19.2.11 で検証されている。
React
React のアプリケーションでは、axios のインターセプタを使って Cookie からヘッダへ渡すパターンを実装できる。
// csrf-protection.js
import axios from 'axios';
// Function to get the CSRF token from cookies
const getCsrfToken = () => {
const tokenCookie = document.cookie
.split('; ')
.find(cookie => cookie.startsWith('XSRF-TOKEN='));
return tokenCookie ? tokenCookie.split('=')[1] : '';
};
// Create an axios instance with interceptors
const api = axios.create();
// Add a request interceptor to include the CSRF token in headers
api.interceptors.request.use(config => {
// Only add for state-changing methods
if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
config.headers['X-CSRF-Token'] = getCsrfToken();
}
return config;
});
export default api;
Axios
Axios では、POST、PUT、DELETE、PATCH の動作に対する既定のヘッダを設定できる。
<script type="text/javascript">
const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");
// Set CSRF token for state-changing methods
axios.defaults.headers.post['X-CSRF-Token'] = csrf_token;
axios.defaults.headers.put['X-CSRF-Token'] = csrf_token;
axios.defaults.headers.delete['X-CSRF-Token'] = csrf_token;
axios.defaults.headers.patch['X-CSRF-Token'] = csrf_token;
// For TRACE method
axios.defaults.headers.trace = {
'X-CSRF-Token': csrf_token
};
// Alternative: Using interceptors for all requests
axios.interceptors.request.use(config => {
// Only add for state-changing methods
if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
config.headers['X-CSRF-Token'] = csrf_token;
}
return config;
});
</script>
このコードの断片は Axios 1.9.0 で検証されている。
jQuery
jQuery は $.ajaxSetup() という API を公開しており、AJAX のリクエストに X-CSRF-Token ヘッダを加えるのに使える。
以下で定義する csrfSafeMethod() 関数は安全な HTTP メソッドを除外し、安全でない HTTP メソッドにのみヘッダを加える。
次のコードの断片を採り入れれば、jQuery がすべてのリクエストのヘッダへ自動的にトークンを加えるよう設定できる。 これは AJAX に基づくアプリケーションに、単純で便利な CSRF 保護を与える。
<script type="text/javascript">
const csrf_token = $('meta[name="csrf-token"]').attr('content');
const csrfSafeMethod = method => {
// these HTTP methods do not require CSRF protection
return /^(GET|HEAD|OPTIONS)$/i.test(method);
};
$.ajaxSetup({
beforeSend: (xhr, settings) => {
if (!csrfSafeMethod(settings.type) && !settings.crossDomain) {
xhr.setRequestHeader("X-CSRF-Token", csrf_token);
}
}
});
</script>
このコードの断片は jQuery 3.7.1 で検証されている。
CSRF 保護のための TypeScript のユーティリティ
TypeScript では、CSRF 保護のために強く型付けされたユーティリティを作れる。 CSRF トークンの管理のための再利用可能なユーティリティのモジュールを示す。
// csrf-protection.ts
/**
* Configuration options for CSRF protection
*/
interface CSRFOptions {
/** Cookie name where the CSRF token is stored */
cookieName: string;
/** HTTP header name to use when sending the token */
headerName: string;
/** HTTP methods that require CSRF protection */
unsafeMethods: string[];
}
/**
* Default configuration for CSRF protection
*/
const DEFAULT_CSRF_OPTIONS: CSRFOptions = {
cookieName: 'XSRF-TOKEN',
headerName: 'X-CSRF-Token',
unsafeMethods: ['POST', 'PUT', 'PATCH', 'DELETE']
};
/**
* CSRF Protection utility class
*/
export class CSRFProtection {
private options: CSRFOptions;
constructor(options: Partial<CSRFOptions> = {}) {
this.options = { ...DEFAULT_CSRF_OPTIONS, ...options };
}
/**
* Extract CSRF token from cookies
* @returns The CSRF token or empty string if not found
*/
public getToken(): string {
const cookieValue = document.cookie
.split('; ')
.find(cookie => cookie.startsWith(`${this.options.cookieName}=`));
return cookieValue ? cookieValue.split('=')[1] : '';
}
/**
* Check if the given HTTP method requires CSRF protection
*/
public requiresProtection(method: string): boolean {
return this.options.unsafeMethods.includes(method.toUpperCase());
}
/**
* Add CSRF token to the provided headers object if needed
*/
public addTokenToHeaders(method: string, headers: Record<string, string>): Record<string, string> {
if (this.requiresProtection(method)) {
const token = this.getToken();
if (token) {
headers[this.options.headerName] = token;
}
}
return headers;
}
}
// Usage example:
// const csrfProtection = new CSRFProtection();
// const headers = csrfProtection.addTokenToHeaders('POST', {});
TypeScript を使う Angular
Angular は TypeScript で作られているので、強く型付けされた CSRF 保護に自然に適する。 以下の例は、TypeScript で Angular の CSRF 保護を設定する方法を示す。
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
import { routes } from './app.routes';
// Configure CSRF protection with custom options
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN', // Name of cookie containing token
headerName: 'X-XSRF-TOKEN' // Header name for token submission
})
),
provideRouter(routes)
]
};
CSRF のトークンを扱う独自の HTTP インターセプタの例を示す。
// csrf.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
private readonly TOKEN_HEADER_NAME = 'X-CSRF-Token';
private readonly SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'];
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Skip CSRF protection for safe methods
if (this.SAFE_METHODS.includes(request.method)) {
return next.handle(request);
}
// Get token from cookie
const token = this.getTokenFromCookie();
if (token) {
// Clone the request and add the CSRF token header
const modifiedRequest = request.clone({
headers: request.headers.set(this.TOKEN_HEADER_NAME, token)
});
return next.handle(modifiedRequest);
}
return next.handle(request);
}
private getTokenFromCookie(): string {
const tokenCookie = document.cookie
.split('; ')
.find(cookie => cookie.startsWith('XSRF-TOKEN='));
return tokenCookie ? tokenCookie.split('=')[1] : '';
}
}
TypeScript を使う React
axios を使う React のアプリケーション向けの TypeScript の実装を示す。
// csrf-axios.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
/**
* Create an axios instance with CSRF protection
*/
export function createCSRFProtectedAxios(
options: {
baseURL?: string;
csrfHeaderName?: string;
csrfCookieName?: string;
} = {}
): AxiosInstance {
const {
baseURL = '',
csrfHeaderName = 'X-CSRF-Token',
csrfCookieName = 'XSRF-TOKEN'
} = options;
// Create axios instance
const instance = axios.create({ baseURL });
// Add CSRF token interceptor
instance.interceptors.request.use((config: AxiosRequestConfig) => {
// Only add for non-GET requests
if (config.method && !['get', 'head', 'options'].includes(config.method.toLowerCase())) {
const token = getCsrfToken(csrfCookieName);
if (token && config.headers) {
config.headers[csrfHeaderName] = token;
}
}
return config;
});
return instance;
}
/**
* Extract CSRF token from cookies
*/
function getCsrfToken(cookieName: string): string {
const tokenCookie = document.cookie
.split('; ')
.find(cookie => cookie.startsWith(`${cookieName}=`));
return tokenCookie ? tokenCookie.split('=')[1] : '';
}
fetch API を TypeScript で使う React のアプリケーション向けの実装を示す。
// csrf-fetch.ts
/**
* Interface for CSRF protection options
*/
interface CSRFFetchOptions {
csrfHeaderName: string;
csrfCookieName: string;
baseUrl: string;
}
/**
* A wrapper around fetch API with CSRF protection
*/
export class CSRFProtectedFetch {
private options: CSRFFetchOptions;
constructor(options: Partial<CSRFFetchOptions> = {}) {
this.options = {
csrfHeaderName: 'X-CSRF-Token',
csrfCookieName: 'XSRF-TOKEN',
baseUrl: '',
...options
};
}
/**
* Performs a fetch request with CSRF protection
*/
public async fetch<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
const { method = 'GET' } = options;
const fullUrl = `${this.options.baseUrl}${url}`;
// Create headers with CSRF token for unsafe methods
const headers = new Headers(options.headers);
if (!['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())) {
const token = this.getCsrfToken();
if (token) {
headers.append(this.options.csrfHeaderName, token);
}
}
// Perform request
const response = await fetch(fullUrl, {
...options,
headers
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
/**
* Shorthand for POST requests
*/
public async post<T>(url: string, data: any, options: RequestInit = {}): Promise<T> {
return this.fetch<T>(url, {
...options,
method: 'POST',
body: JSON.stringify(data),
headers: {
...options.headers,
'Content-Type': 'application/json'
}
});
}
/**
* Extract CSRF token from cookies
*/
private getCsrfToken(): string {
const tokenCookie = document.cookie
.split('; ')
.find(cookie => cookie.startsWith(`${this.options.csrfCookieName}=`));
return tokenCookie ? tokenCookie.split('=')[1] : '';
}
}
関連するチートシートの参考資料
CSRF
- OWASP Cross-Site Request Forgery (CSRF)
- PortSwigger Web Security Academy
- Mozilla Web Security Cheat Sheet
- Common CSRF Prevention Misconceptions
- Robust Defenses for Cross-Site Request Forgery
- Java 向け。OWASP CSRF Guard または Spring Security
- PHP と Apache 向け。CSRFProtector Project
- Angular 向け。Cross-Site Request Forgery (XSRF) Protection