CSRF とトークン
テクノロジ系 / session-cookie (session / cookie / login / CSRF)
CSRF とトークン
Lesson 13 / Chapter 5
CSRF って何
Cross-Site Request Forgery = 「他サイトを経由した、なりすましリクエスト送信攻撃」
1. ユーザーが bank.example にログイン済 (Session Cookie あり) 2. 攻撃者の罠サイト evil.example をユーザーが踏む 3. evil.example の中に隠された <form action="bank.example/transfer"> 4. JS が自動 submit → ブラウザは bank.example の Cookie を一緒に送る 5. bank.example 側は「正規のログインユーザーの操作」と誤認して送金実行
→ Cookie が 自動で送られる という仕組みを悪用する攻撃。
どう防ぐか — トークン方式
「そのサーバーが自分で発行したフォームでなければ受け付けない」を実現する。
1. /form アクセス時、サーバーがランダム文字列を生成 → $_SESSION に保存 2. <input type="hidden" name="csrf_token" value="ABC123..."> を埋める 3. ユーザーが submit → サーバーは送信値と Session 内の値を比較 4. 一致したら処理続行、違ったら拒否
攻撃者は Session 内のトークン を知らないので、評価できない。
トークン生成 (PHP)
<?php session_start(); // 32 バイト = 16 進 64 文字のランダム値 $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); echo '<form method="post" action="/submit">'; echo '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">'; echo '<input name="message">'; echo '<button>送信</button>'; echo '</form>';
random_bytes(32)は 暗号論的に安全な乱数bin2hex()で 16 進文字列 (64 文字) に変換- form の hidden に埋めて、submit 時にサーバーへ戻ってくる
トークン検証 (PHP)
<?php
session_start();
$sent = $_POST['csrf_token'] ?? '';
$saved = $_SESSION['csrf_token'] ?? '';
// ✅ hash_equals で安全に比較する
if (!hash_equals($saved, $sent)) {
http_response_code(403);
echo "CSRF 検証失敗\n";
exit;
}
// 検証成功 → 本来の処理に進む
echo "OK\n";なぜ hash_equals() か:
==や===は 比較に掛かる時間が値で変わる (タイミング攻撃の足掛かり)hash_equals()は 長さに関わらず一定時間 で比較する設計
トークン使い切りの作法 (ベストプラクティス)
<?php
// 検証 OK だったら即破棄して使い回しを防ぐ
if (hash_equals($saved, $sent)) {
unset($_SESSION['csrf_token']); // 一度きり
// 処理続行
}- 同じトークンを何度も受け付けると、リプレイ攻撃の足掛かりになる
- 通常は 1 フォーム = 1 トークン、検証後は破棄
- ユーザーが連投したい場合は、検証後に 新しいトークンを発行 して返す
採点用: 1 スクリプトで生成 → 検証を擬似化
<?php
// (採点用スタブ ...)
// === リクエスト 1: form を返す ===
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// === リクエスト 2: form 送信を受ける ===
parse_str(trim(fgets(STDIN) ?: ''), $_POST);
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
echo "OK\n";
} else {
echo "NG\n";
}ドリルでは stdin から「攻撃者から見た送信値」を渡し、合致するか合致しないかを試す。
このチャプターでできるようになること
✅ CSRF が「他サイト経由でなりすますリクエスト送信攻撃」と説明できる
✅ random_bytes + bin2hex でトークンを生成できる
✅ $_SESSION['csrf_token'] に保存して form の hidden に埋められる
✅ hash_equals() で安全に比較できる
✅ 「使い切り」のお作法を知っている
→ ドリルへ
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ドリル 01 — CSRF トークンを生成して Session に保存
問題
CSRF 防御の出発点 — ランダムなトークンを生成して $_SESSION['csrf_token'] に保存 する処理を書いてください。
random_bytes(32)でランダム 32 バイトを取得bin2hex()で 16 進文字列 (64 文字) に変換$_SESSION['csrf_token']に保存- 保存したトークンの 長さ を
"length: 64"の形式で 1 行出力する (採点のためにトークン本体ではなく長さを出す)
期待される出力:
length: 64
採点
php scripts/grade.php topics/session-cookie/ch05-csrf/drill/01-token-generate/
ヒント
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); echo "length: " . strlen($_SESSION['csrf_token']) . "\n";
本物の Web で確認したい場合
cd topics/session-cookie/ch05-csrf/drill/01-token-generate/ php -S localhost:8000 answer.php
ブラウザのソース表示で hidden input にトークンが入っていることを確認 (本ドリルでは form を出力しないが、本物の Web では <input type="hidden" name="csrf_token" value="..."> を出す)。
ドリル 02 — CSRF トークンを検証
問題
サーバー側が保持しているトークンと、フォーム送信で受け取ったトークンを hash_equals() で比較し、一致すれば OK、違えば NG を 1 行出力してください。
- サーバー側の正解トークンは固定値
EXPECTED_TOKEN_ABC123(採点用に決め打ち) - 起動時に
$_SESSION['csrf_token'] = 'EXPECTED_TOKEN_ABC123'をセットしておくこと - 標準入力 1 行目を「フォーム送信値」とみなし
$_POSTにparse_strで展開 $_POST['csrf_token']と$_SESSION['csrf_token']をhash_equals()で比較
このドリルの入力例 (tests/input.txt):
csrf_token=EXPECTED_TOKEN_ABC123
期待される出力:
OK
別パターン (攻撃想定): csrf_token=AAAA → NG
採点
php scripts/grade.php topics/session-cookie/ch05-csrf/drill/02-token-verify/
ヒント
$_SESSION['csrf_token'] = 'EXPECTED_TOKEN_ABC123';
parse_str(trim(fgets(STDIN) ?: ''), $_POST);
$sent = $_POST['csrf_token'] ?? '';
$saved = $_SESSION['csrf_token'] ?? '';
if (hash_equals($saved, $sent)) {
echo "OK\n";
} else {
echo "NG\n";
}なぜ hash_equals か
== や === は比較に掛かる時間が値で変わる (タイミング攻撃の足掛かり)。hash_equals() は 一定時間で比較 する設計。