# ch05 — CSRF とトークン

Source: https://php-school.pages.dev/topics/session-cookie/ch05-csrf/
Site: AIと学ぶPHPの学校 (https://php-school.pages.dev)
Lesson: セッション・Cookie・ログイン
Level: L4

Reference (official): https://www.php.net/manual/ja/book.session.php

## 本文

# ch05 — CSRF とトークン

## 概要

**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()` で**安全に比較**できる

## 所要時間

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

## ドリル

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

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

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

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

## スライド

---
marp: true
theme: dojo
paginate: true
size: 16:9
---

<!--
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
<?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
<?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
<?php
// 検証 OK だったら即破棄して使い回しを防ぐ
if (hash_equals($saved, $sent)) {
    unset($_SESSION['csrf_token']);  // 一度きり
    // 処理続行
}
```

- 同じトークンを何度も受け付けると、リプレイ攻撃の足掛かりになる
- 通常は **1 フォーム = 1 トークン**、検証後は破棄
- ユーザーが連投したい場合は、検証後に **新しいトークンを発行** して返す

---

## 採点用: 1 スクリプトで生成 → 検証を擬似化

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

→ ドリルへ
