CSRF を 3 つの関門で考える(CORS / SameSite / Origin・Sec-Fetch-Site 検証)
クッキー付き CSRF が刺さるのは「ブラウザが送る」「クッキーが付く」「サーバーが受け取る」の 3 つを全部突破されたとき。どこで止めるかを意識して設計し、サーバー側では Origin / Sec-Fetch-Site 検証を入れる。
3 つの関門(@calloc134 の整理)
- ブラウザが送信を許容するか: CORS の有無。CORS があっても GET や simple request(
application/x-www-form-urlencoded等)判定でプリフライトなしに送れてしまうか。 - クッキーが付属するか:
SameSite属性(Lax/Strict)。 - サーバーが受け取るか:
OriginヘッダやSec-Fetch-Siteの検証があるか。
実装例(サーバー側の検証)
ts
// 状態変更リクエストで Origin / Sec-Fetch-Site を検証する middleware の例
const ALLOWED = new Set(['https://app.example.com']);
function csrfGuard(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const site = req.headers['sec-fetch-site'];
if (site && site !== 'same-origin' && site !== 'none') return res.status(403).end();
const origin = req.headers['origin'];
if (origin && !ALLOWED.has(origin)) return res.status(403).end();
if (!site && !origin) return res.status(403).end(); // 両方欠けたら拒否(要件次第)
next();
}- セッションクッキーは
SameSite=Lax以上 +Secure+HttpOnly。 - GET で状態変更しない。
議論のポイント
- 決定打は Origin ヘッダ検証という意見(@calloc134)がある一方、OWASP は CSRF トークンを主要対策として推奨。古いブラウザや Origin が付かないケースを考慮するならトークン併用が無難。
- Next.js の Server Actions は Origin と Host の比較で CSRF を防ぐ設計だが、その前提を疑う声もある(@ysknsid25)。リバースプロキシ構成では Host / X-Forwarded-Host の扱いを確認。