CSRF(クロスサイトリクエストフォージェリ)は、ログイン中のユーザーのブラウザに、別のサイトから意図しないリクエストを送らせる攻撃です。ブラウザがCookieを自動で付けて送るため、サーバーは本人の操作だと誤認して送金や設定変更を実行してしまいます。対策の基本は、状態を変えるリクエストにCSRFトークンやFetch Metadataの確認を組み込むことで、SameSite属性は補助として重ねます。
CSRFとは
MDNによると、次の3つの条件がそろうとCSRFが成立します。
CSRFが成立する条件
| 条件 | 具体例 |
|---|---|
| HTTPリクエストでサーバーの状態を変える | 送金、パスワードやメールアドレスの変更、投稿、退会 |
| 認証をCookieだけに頼っている | セッションCookieが付いていれば本人の操作とみなす |
| リクエストのパラメータを攻撃者が推測できる | 宛先と金額だけで送金が実行される |
OWASP Top 10:2025では、CSRF(CWE-352)はA01「Broken Access Control」に含まれています。IPAの「安全なウェブサイトの作り方」でも、CSRFは取り上げられている11種類の脆弱性のひとつです。
攻撃が成立する仕組み
攻撃者は、罠のページに標的サイトへ送信するフォームを仕込みます。ユーザーが標的サイトにログインしたまま罠のページを開くと、フォームが自動送信されます。
<!-- 攻撃者のページ(例) -->
<form action="https://my-bank.example.org/transfer" method="POST">
<input type="hidden" name="recipient" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
document.querySelector("form").submit();
</script>
状態を変える処理をGETで受け付けていると、フォームすら不要です。<img> タグの src に送金用のURLを書くだけで、ページを開いた瞬間にリクエストが送られます。
CSRF攻撃の流れ
- ユーザーが標的サイトにログインブラウザにセッションCookieが残る
- 罠のページを開くメールやSNSのリンクなどから
- 隠しフォームが自動送信ブラウザがCookieを付けて送る
- 標的サイトが処理を実行本人の操作と区別できない
XSSと違い、攻撃者はレスポンスの中身を読むことはできません。そのかわり、送金や設定変更のような「実行されたら困る処理」を本人の権限で実行させられます。
主な対策(CSRFトークン)
もっとも基本的な対策は、推測できないトークンをページに埋め込み、状態を変えるリクエストではトークンが一致した場合だけ処理する方法です。IPAも根本的解決として、秘密の値をhiddenパラメータに入れてPOSTで送る方法を挙げています。
トークンを生成してセッションに保存する
暗号論的に安全な乱数で、推測できない値を作ります。
フォームのhiddenフィールドに埋め込む
GETのURLに含めると、Refererヘッダーなどから漏れるおそれがあります。POSTのボディやカスタムヘッダーで送ります。
受信時にセッションの値と照合する
一致しない、または送られてこないリクエストは拒否します。
Node.js(Express)での実装例です。実際のプロジェクトでは、フレームワークに組み込まれたCSRF対策機能があればそちらを使ってください。
import crypto from "node:crypto";
// フォーム表示時:トークンを生成してセッションに保存する
app.get("/settings", (req, res) => {
const token = crypto.randomBytes(32).toString("hex");
req.session.csrfToken = token;
res.render("settings", { csrfToken: token });
});
// 送信時:セッションの値と一致するか確認する
app.post("/settings", (req, res) => {
const sent = Buffer.from(String(req.body.csrfToken ?? ""));
const expected = Buffer.from(String(req.session.csrfToken ?? ""));
if (
expected.length === 0 ||
sent.length !== expected.length ||
!crypto.timingSafeEqual(sent, expected)
) {
return res.status(403).send("Forbidden");
}
// ここで設定を更新する
res.redirect("/settings");
});
<form action="/settings" method="POST">
<input type="hidden" name="csrfToken" value="(サーバーで埋め込んだトークン)" />
<!-- 入力項目 -->
</form>
ステートレスなアプリでは、OWASPはサーバー側の秘密鍵でセッションに紐づく値をHMAC署名する「署名付きダブルサブミットCookie」を推奨しています。Cookieの値を照合するだけの素朴な方式は、Cookieを注入されると破られるため推奨されていません。
Fetch Metadataと単純リクエストの回避
最近のブラウザは、リクエストがどこから来たかを示す Sec-Fetch-Site ヘッダーを送ります。値は same-origin、same-site、cross-site、none(ユーザーが直接開いた場合)のいずれかです。
// クロスサイトからの状態変更を拒否する
app.post("/transfer", (req, res) => {
const site = req.headers["sec-fetch-site"];
if (site !== "same-origin" && site !== "same-site") {
return res.status(403).send("Forbidden");
}
// ここで送金処理を行う
});
OWASPは、Sec-Fetch-Site を主な判断材料にしつつ、ヘッダーがない場合は Origin、次に Referer で確認する方法を示しています。オリジンは完全一致で照合し、null を許可リストに入れないよう注意しています。この方式の前提として、アプリ全体をHTTPSにしておく必要があります。
fetch() で呼ぶAPIでは、リクエストを「単純リクエスト」にしない方法も有効です。Content-Type: application/json やカスタムヘッダーを付けると、クロスオリジンでの送信にはプリフライトが必要になり、既定ではブロックされます。
fetch("/api/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ recipient: "joe", amount: "100" }),
});
フォーム(HTMLの送信)
- CSRFトークンをhiddenフィールドで送る
- Fetch Metadataでクロスサイトを拒否
- 重要な操作は再認証を求める
JavaScriptから呼ぶAPI
- JSONやカスタムヘッダーで単純リクエストにしない
- CORSで信頼するオリジンだけを許可
- Fetch Metadataでクロスサイトを拒否
ただしMDNは、サーバーのCORS設定が送信元のオリジンに対して Access-Control-Allow-Origin と Access-Control-Allow-Credentials の両方を返すと、この保護が無効になると注意しています。サブドメインをワイルドカードで許可するようなCORS設定も避けてください。
SameSite属性とその他の対策
Cookieの SameSite 属性は、クロスサイトのリクエストにCookieを付けるかどうかを制御します。OWASPは、主要なブラウザが既定で SameSite=Lax として扱っていると説明していますが、古いブラウザや組み込みブラウザでは適用されない場合があるとも書いています。
SameSite属性の値と注意点
| 値 | 挙動 | 注意点 |
|---|---|---|
Strict | クロスサイトのリクエストには一切Cookieを付けない | 外部リンクから来たときにログインしていない状態に見えることがある |
Lax | クロスサイトでは、安全なメソッド(GETなど)のトップレベル遷移のときだけ付ける | GETで状態を変える処理があると防げない。メソッドオーバーライドにも注意 |
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict
MDNは、状態を変える操作に使うCookieには Strict、ログイン済みの画面を表示するかどうかの判定に使うCookieには Lax を推奨しています。SameSiteは「オリジン」ではなく「サイト」単位で判定されるため、同じサイトのサブドメインはすべて信頼することになる点も覚えておきたいところです。
このほか、IPAは次の対策も挙げています。
- 重要な処理の直前にパスワードの再入力を求める
- Refererを確認し、正しいページからのリクエストだけを処理する(Refererを送らない設定の利用者は使えなくなる)
- 重要な操作が行われたら登録メールアドレスに通知する(攻撃を防ぐものではないが、早く気づける)
よくある質問
SameSite=Laxが既定なら、CSRF対策は不要ですか?
不要にはなりません。OWASPもMDNも、SameSiteは多層防御のひとつで、単独では十分ではないとしています。Laxではトップレベル遷移のGETにはCookieが付くうえ、古いブラウザでは既定の挙動が適用されない場合もあります。CSRFトークンやFetch Metadataと組み合わせてください。
なぜGETで状態を変えてはいけないのですか?
GETのリクエストは <img> タグやリンクだけで送らせることができ、SameSite=Lax でもトップレベル遷移ならCookieが付いてしまうためです。MDNもOWASPも、状態を変える処理にGETを使わないよう明記しています。まずはセッションCookieに依存して状態を変えるリクエストをすべて洗い出し、POSTなどに寄せてから対策を入れてください。
XSSがあるとCSRF対策はどうなりますか?
OWASPは、XSSがあるとすべてのCSRF対策が無効化され得ると警告しています。同じオリジンで動くスクリプトは、正しいトークンやヘッダーを付けてリクエストを送れるためです。XSSの対策もあわせて確認してください。
APIをトークン認証(Authorizationヘッダー)にしている場合は?
MDNは、Cookieだけで認証していることをCSRFの成立条件に挙げています。ブラウザが自動で付けない認証情報だけで認証しているなら、この条件には当てはまりません。ただし、Cookieと併用している場合は対策が必要です。
出典: MDN「Cross-site request forgery (CSRF)」、OWASP Cross-Site Request Forgery Prevention Cheat Sheet、IPA「安全なウェブサイトの作り方 1.6 CSRF」、IPA「安全なウェブサイトの作り方」、OWASP A01:2025 Broken Access Control