topic: session-cookie (session / cookie / login / CSRF) / ch05

ch05 — CSRF とトークン

学習目標

  • CSRF (Cross-Site Request Forgery) が何を狙う攻撃かを 1 行で言える
  • ランダムなトークンを生成し $_SESSION['csrf_token'] に保存できる
  • 受け取ったフォームのトークンを hash_equals()安全に比較できる

所要時間

スライド 6 分 + ドリル 2 問 = 30 分

ドリル

no 内容
01 トークンを生成して $_SESSION に保存 → 16 進文字数を出力
02 stdin からトークン文字列を受け取り、$_SESSION 内の正解と一致したら OK、違ったら NG

本物の Web で確認したい場合

cd topics/13-session-cookie/ch05-csrf/drill/01-token-generate/
php -S localhost:8000 answer.php

ブラウザでアクセスし、開発者ツールで PHPSESSID Cookie を見て、サーバー側で Session にトークンが保存されていることを確認 (実体はサーバー側ファイル)。

解説

この章の本体解説です(教材スライド由来)。

<!-- LLM_CONTEXT: Lesson 13 / Chapter 5 目的: CSRF 攻撃の概念 / Session トークンで防ぐ手筋 / hash_equals 扱わない: regenerate_id (ch06) / セッション固定化 (ch06) 読み上げ時間目安: 5 分半〜6 分 -->

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>';
▶ 3v4l で実行
  • 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";
▶ 3v4l で実行

なぜ hash_equals() か:

  • =====比較に掛かる時間が値で変わる (タイミング攻撃の足掛かり)
  • hash_equals()長さに関わらず一定時間 で比較する設計

トークン使い切りの作法 (ベストプラクティス)

<?php
// 検証 OK だったら即破棄して使い回しを防ぐ
if (hash_equals($saved, $sent)) {
    unset($_SESSION['csrf_token']);  // 一度きり
    // 処理続行
}
▶ 3v4l で実行
  • 同じトークンを何度も受け付けると、リプレイ攻撃の足掛かりになる
  • 通常は 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";
}
▶ 3v4l で実行

ドリルでは stdin から「攻撃者から見た送信値」を渡し、合致するか合致しないかを試す。


このチャプターでできるようになること

✅ CSRF が「他サイト経由でなりすますリクエスト送信攻撃」と説明できる ✅ random_bytes + bin2hex でトークンを生成できる ✅ $_SESSION['csrf_token'] に保存して form の hidden に埋められる ✅ hash_equals() で安全に比較できる ✅ 「使い切り」のお作法を知っている

→ ドリルへ

演習問題の詳細

この章の演習問題の内容を読めます。実際に手元で解くには教材リポジトリを clone してください。

ドリル 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/13-session-cookie/ch05-csrf/drill/01-token-generate/

ヒント

$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
echo "length: " . strlen($_SESSION['csrf_token']) . "\n";
▶ 3v4l で実行

本物の Web で確認したい場合

cd topics/13-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 行目を「フォーム送信値」とみなし $_POSTparse_str で展開
  • $_POST['csrf_token']$_SESSION['csrf_token']hash_equals() で比較

このドリルの入力例 (tests/input.txt):

csrf_token=EXPECTED_TOKEN_ABC123

期待される出力:

OK

別パターン (攻撃想定): csrf_token=AAAANG

採点

php scripts/grade.php topics/13-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";
}
▶ 3v4l で実行

なぜ hash_equals

===== は比較に掛かる時間が値で変わる (タイミング攻撃の足掛かり)。hash_equals()一定時間で比較 する設計。

演習問題(2問)

  1. ドリル 01 — CSRF トークンを生成して Session に保存

    README.md starter.php answer.php

  2. ドリル 02 — CSRF トークンを検証

    README.md starter.php answer.php

サイト内で問題文・雛形・解答例を確認できます。実際に手元で解くには教材リポジトリを clone してください。