ch05 6章中 5章目

ch05 — CSRF とトークン

スライドで見る

この章を AI と学ぶ

💬 AI に聞く質問例 3 件
  1. 「ch05 — CSRF とトークン」のつまずきやすいポイントを、初学者向けに3つ教えてください

    ChatGPT Claude Gemini

  2. 「ch05 — CSRF とトークン」について、自分の手で試せる練習問題を3つ出してください (難易度を上げながら)

    ChatGPT Claude Gemini

  3. 「ch05 — CSRF とトークン」を学んだ後、次に学ぶと良いトピックは何ですか?理由も教えてください

    ChatGPT Claude Gemini

概要

CSRF (Cross-Site Request Forgery) は、ログイン済みユーザーのブラウザを 悪用 して、本人の意図しないリクエストをサーバーに送らせる攻撃。被害者が銀行サイトにログインしたまま攻撃者の悪意ページを開くと、仕込まれた <form action="https://bank.example/transfer"> が自動送信され、被害者の Cookie と一緒に届く。サーバーから見れば「正規ユーザーの送金」にしか見えない。

対策の標準形が CSRF トークン。フォーム表示時に ランダム文字列 を生成して $_SESSION['csrf_token'] に保存し、同じ値を <input type="hidden" name="csrf_token"> にも埋め込む。送信時に $_POST['csrf_token']$_SESSION['csrf_token']hash_equals() で比較 し、不一致なら拒否。攻撃者のフォームには正規 Session 内のトークンが書けないので攻撃が成立しない。

比較に == ではなく hash_equals() を使うのは タイミング攻撃 対策。== だと一致する prefix の長さで応答時間が微妙に変わり、攻撃者が時間差から「先頭何文字までは合っている」を推測できる。hash_equals は必ず全文字を比較して時間差を消す、Web セキュリティの定番イディオム。

学習目標

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

ドリル

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

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

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

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

解説スライド 全 8 枚

右の ボタン キー(またはスワイプ)で次のスライドに進めます。 下の「縦表示」を開くと全スライドを一気に読めます。

  1. CSRF とトークン

    Lesson 13 / Chapter 5

  2. 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 が 自動で送られる という仕組みを悪用する攻撃。

  3. どう防ぐか — トークン方式

    そのサーバーが自分で発行したフォームでなければ受け付けない」を実現する。

    1. /form アクセス時、サーバーがランダム文字列を生成 → $_SESSION に保存
    2. <input type="hidden" name="csrf_token" value="ABC123..."> を埋める
    3. ユーザーが submit → サーバーは送信値と Session 内の値を比較
    4. 一致したら処理続行、違ったら拒否

    攻撃者は Session 内のトークン を知らないので、評価できない。

  4. トークン生成 (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 時にサーバーへ戻ってくる
  5. トークン検証 (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()長さに関わらず一定時間 で比較する設計
  6. トークン使い切りの作法 (ベストプラクティス)

    <?php
    // 検証 OK だったら即破棄して使い回しを防ぐ
    if (hash_equals($saved, $sent)) {
        unset($_SESSION['csrf_token']);  // 一度きり
        // 処理続行
    }
    • 同じトークンを何度も受け付けると、リプレイ攻撃の足掛かりになる
    • 通常は 1 フォーム = 1 トークン、検証後は破棄
    • ユーザーが連投したい場合は、検証後に 新しいトークンを発行 して返す
  7. 採点用: 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 から「攻撃者から見た送信値」を渡し、合致するか合致しないかを試す。

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

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

    → ドリルへ

1 / 8
📜 スライドを全部一気に読む(縦表示)

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() で安全に比較できる ✅ 「使い切り」のお作法を知っている

→ ドリルへ

この章で取り組む 演習問題の流れ (2ステップ)
  1. 01 ドリル 01 — CSRF トークンを生成して Session に保存 README.md starter.php answer.php
  2. 02 ドリル 02 — CSRF トークンを検証 README.md starter.php answer.php

演習問題の詳細

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

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

問題

CSRF 防御の出発点 — ランダムなトークンを生成して $_SESSION['csrf_token'] に保存 する処理を書いてください。

  • random_bytes(32) でランダム 32 バイトを取得
  • bin2hex() で 16 進文字列 (64 文字) に変換
  • $_SESSION['csrf_token'] に保存
  • 保存したトークンの 長さ"length: 64" の形式で 1 行出力する (採点のためにトークン本体ではなく長さを出す)

期待される出力:

length: 64

ヒント

$_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="..."> を出す)。

🧪 ブラウザで試す

スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。

PHP ランタイム未読込

          

        

ドリル 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

ヒント

$_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()一定時間で比較 する設計。

🧪 ブラウザで試す

スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。

PHP ランタイム未読込

          

        

演習問題(2問)

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

    README.md starter.php answer.php

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

    README.md starter.php answer.php

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

完了にすると進捗に反映されます