CSRF攻撃の基礎と対策
Cross-Site Request Forgeryの仕組みと、CSRFトークン・SameSite Cookieによる防御方法を理解する
TL;DR
- CSRFは、ログイン済みユーザーのブラウザを経由して、攻撃者が意図したリクエストを勝手に送らせる攻撃
- ブラウザはリクエスト先のサイトに対してCookieを自動で付与するため、被害者が意図せず操作を実行させられる
- 主な対策は CSRFトークン と SameSite Cookie属性 の2つ
CSRFとは
CSRF(Cross-Site Request Forgery)は、攻撃者が用意した罠サイトやリンクを通じて、被害者のブラウザからターゲットサイトへ不正なリクエストを送らせる攻撃。
ブラウザは同一ドメインのCookieを自動で付与してリクエストを送る仕様になっている。これを悪用することで、被害者が意図せずログイン済みの別サービスへリクエストを送ることになる。
攻撃の流れ
典型的なシナリオ
1. 被害者が bank.example.com にログインしている(セッションCookieが存在する)
2. 攻撃者が用意した悪意あるサイト evil.example.com を被害者が開く
3. 悪意あるページに埋め込まれたフォームやimgタグが自動で発火する
4. ブラウザが bank.example.com への送金リクエストをCookie付きで送信する
5. サーバーは正規のセッションCookieを確認し、リクエストを受け付けてしまう攻撃コードの例
<!-- 攻撃者のページに埋め込まれたフォーム(自動送信) -->
<form action="https://bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="100000" />
</form>
<script>document.forms[0].submit();</script><!-- GETリクエストが副作用を持つ場合はimgタグでも攻撃できる -->
<img src="https://bank.example.com/transfer?to=attacker&amount=100000" />被害者が evil.example.com を開いた瞬間に、bank.example.com への送金リクエストが自動送信される。サーバー側はCookieが正規のものであるため、正当なリクエストと区別できない。
XSSとの違い
混同しやすいので整理しておく。
| CSRF | XSS | |
|---|---|---|
| 攻撃の起点 | 別サイト(クロスサイト)から | 同一サイトに悪意あるスクリプトを埋め込む |
| 被害 | 被害者が意図しない操作を実行させられる | 攻撃者のスクリプトが被害者のブラウザ上で動く |
| Cookieへの直接アクセス | できない | できる(httpOnly でない場合) |
CSRFはリクエストを「送らせる」攻撃で、Cookieの「読み取り」はできない。
対策1: CSRFトークン
サーバーがランダムなトークンをセッションに紐づけて発行し、フォームやAPIリクエストにそのトークンを含めることを要求する方法。
仕組み
1. サーバーがセッションにCSRFトークンを保存し、レスポンスに含める
2. クライアントはリクエスト送信時にそのトークンをヘッダー or フォームに含める
3. サーバーはリクエストのトークンとセッションのトークンを照合する
4. 攻撃者は被害者のCSRFトークンを知らないため、有効なリクエストを偽造できない攻撃者が罠サイトからリクエストを送っても、正しいCSRFトークンを含められないため弾かれる。
NestJSでの実装例(csurf)
npm install csurf cookie-parser// main.ts
import * as cookieParser from "cookie-parser";
import * as csurf from "csurf";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.use(
csurf({
cookie: {
httpOnly: true,
sameSite: "strict",
},
})
);
await app.listen(3000);
}// csrf.controller.ts
import { Controller, Get, Req, Res } from "@nestjs/common";
import { Request, Response } from "express";
@Controller("csrf")
export class CsrfController {
@Get("token")
getToken(@Req() req: Request, @Res() res: Response) {
// クライアントに渡すトークンを生成(csurfが req.csrfToken() を付与する)
res.json({ csrfToken: req.csrfToken() });
}
}// フロントエンド側(例: fetch)
const { csrfToken } = await fetch("/csrf/token").then((r) => r.json());
await fetch("/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken, // ← ヘッダーにトークンを付与
},
body: JSON.stringify({ to: "someone", amount: 1000 }),
});対策2: SameSite Cookie属性
Cookieに SameSite 属性を設定することで、クロスサイトリクエスト時のCookieの送信を制御できる。
| 値 | 動作 |
|---|---|
Strict | 別サイトからのリクエストには一切Cookieを送らない |
Lax | 別サイトからのGETナビゲーション(リンククリック)にはCookieを送るが、POSTには送らない |
None | 制限なし(Secure 属性が必須) |
// NestJSでセッションCookieにSameSiteを設定する例
res.cookie("session", token, {
httpOnly: true,
secure: true, // HTTPS必須(SameSite: NoneはSecureが必須)
sameSite: "lax", // 多くのケースで推奨
});LaxとStrictの使い分け
Strict: セキュリティは最も強いが、外部サイトからのリンクでセッションが引き継がれない(ログイン済みなのに未ログインになる)Lax: GETナビゲーションはCookieを送るため、ユーザー体験を損なわずCSRF(主にPOSTでの攻撃)を防げる
多くの場合 Lax が現実的なバランスとなる。
対策3: OriginヘッダーとRefererヘッダーの検証
ブラウザはリクエスト時に Origin または Referer ヘッダーを自動付与する。サーバー側でこれらを確認し、予期しないオリジンからのリクエストを弾く方法。
// NestJSのミドルウェアでOriginを検証する例
@Injectable()
export class CsrfOriginGuard implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const allowedOrigins = ["https://app.example.com"];
const origin = req.headers["origin"] ?? req.headers["referer"];
if (req.method !== "GET" && !allowedOrigins.some((o) => origin?.startsWith(o))) {
throw new ForbiddenException("Invalid origin");
}
next();
}
}ただしOrigin/Refererは省略されるケースもあるため、これ単独ではなくCSRFトークンやSameSiteとの組み合わせが推奨。
SPAとCSRF
React/Next.jsなどのSPAでAPIをJWTで認証している場合、トークンをどこに保存するかによって状況が変わる。
localStorageにJWTを保存している場合
ブラウザはクロスサイトリクエスト時にlocalStorageの内容を自動送信しない。そのため、認証にCookieを使わないSPAはCSRFの影響を受けにくい。
ただしXSSに弱い(JavaScriptからlocalStorageを読み取れる)。
httpOnly CookieにJWTを保存している場合
JavaScriptからCookieを読み取れないためXSSに強いが、ブラウザが自動でCookieを送るためCSRF対策が必要になる。
保存場所 XSSリスク CSRFリスク
localStorage 高 低(Cookie未使用)
httpOnly Cookie 低 高(要CSRF対策)httpOnly Cookieを使うなら SameSite: Lax か SameSite: Strict を必ず設定する。
まとめ
| 対策 | 有効なケース |
|---|---|
| CSRFトークン | セッションベース認証・フォームベースのアプリ |
| SameSite Cookie | モダンブラウザ環境(現在の主流対策) |
| Originヘッダー検証 | APIサーバーでの補完的な対策 |
現在のモダンブラウザでは SameSite: Lax がデフォルトになっている場合も多く、これだけで多くのCSRF攻撃を防げる。ただし、セッションベースの認証では明示的な設定とCSRFトークンの組み合わせを推奨する。