PHP Tips | 文字列処理:ランダム・生成 – CSRF トークン生成

PHP PHP
スポンサーリンク

まず「CSRF トークン生成」で何を守りたいのか

CSRF トークンは、一言でいうと「フォーム送信が“本当にそのユーザーの意思で行われたものか”を確認するための秘密の合言葉」です。

例えば、ユーザーがログイン中に、悪意あるサイトが勝手に「あなたのブラウザから」銀行振込フォームを送信させる、という攻撃が CSRF(Cross-Site Request Forgery)です。見た目は「正しいユーザーのブラウザからのリクエスト」なので、サーバー側は区別がつきません。

そこで、「そのフォームを表示したときにだけ知っている秘密のトークン」を埋め込んでおき、送信時にそれをチェックすることで、「本物のフォーム送信かどうか」を見分けます。この“秘密のトークン”を安全に生成・保存・検証するのが、CSRF トークン生成ユーティリティの役割です。


CSRF トークンに必要な性質を整理する

CSRF トークンには、最低限次のような性質が必要です。

推測されにくいこと。
ユーザーごと、セッションごとに異なること。
サーバー側で「正しい値」を覚えておけること。

特に重要なのが「推測されにくさ」です。mt_rand()uniqid() のような“なんとなくランダムっぽい”ものではなく、暗号論的に安全な乱数から作る必要があります。PHP では random_bytes()random_int() がその役割を担います。

また、「どこに保存するか」も大事です。典型的には「セッション」に保存します。セッションはユーザーごとに分かれているので、「このユーザーの CSRF トークンはこれ」という対応付けが自然にできます。


コアとなる CSRF トークン生成関数を作る

トークンの中身は「ランダムバイト列+エンコード」

まずは、トークンそのものを生成する関数を作ります。やることはシンプルで、暗号論的に安全なランダムバイト列を作り、それを文字列として扱いやすい形(16進や Base64)に変換するだけです。

/**
 * CSRF トークンを生成する(生のトークン文字列)
 */
function generate_csrf_token(int $bytes = 32): string
{
    // 暗号論的に安全な乱数を生成
    $random = random_bytes($bytes);

    // 16進文字列にして返す(URL やフォームに載せやすい)
    return bin2hex($random);
}
PHP

random_bytes(32) は 32 バイト = 256 ビットのランダム値です。これを 16進文字列にすると 64 文字になります。CSRF トークンとしては十分すぎる長さとランダム性です。


セッションと組み合わせて「発行」と「検証」を設計する

セッションにトークンを保存する

CSRF トークンは「ユーザーごと」に紐づけたいので、セッションに保存するのが定番です。まずはセッションを開始し、トークンを生成してセッションに入れます。

session_start();

/**
 * セッションに CSRF トークンをセットし、その値を返す
 */
function issue_csrf_token(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = generate_csrf_token();
    }

    return $_SESSION['csrf_token'];
}
PHP

ここでは「なければ作る」という形にしています。フォームごとに毎回変える設計もありますが、まずは「セッションごとに 1 つ」のシンプルな形からで十分です。

フォームにトークンを埋め込む

発行したトークンは、フォームの hidden フィールドとして埋め込みます。

session_start();
$csrfToken = issue_csrf_token();
?>
<form method="post" action="/submit.php">
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
    <!-- 他の入力項目 -->
    <button type="submit">送信</button>
</form>
PHP

この hidden フィールドが、「このフォームを表示したときにだけ知っている秘密の合言葉」になります。


送信時に CSRF トークンを検証する

検証の流れを言葉で整理する

POST されたときにやりたいことは、次の 2 点です。

セッションに保存されているトークンと、フォームから送られてきたトークンが一致しているか。
トークンがそもそも送られてきているか(空ではないか)。

これを満たしていれば、「少なくともこのフォームは、正しいセッションを持つブラウザから送られた」と判断できます。

実装例:検証関数

session_start();

/**
 * CSRF トークンを検証する
 */
function validate_csrf_token(?string $tokenFromRequest): bool
{
    if (!isset($_SESSION['csrf_token'])) {
        return false;
    }

    if ($tokenFromRequest === null || $tokenFromRequest === '') {
        return false;
    }

    $stored = $_SESSION['csrf_token'];

    // 比較には hash_equals を使う(タイミング攻撃対策)
    return hash_equals($stored, $tokenFromRequest);
}
PHP

実際のハンドラ側では、こう使います。

session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? null;

    if (!validate_csrf_token($token)) {
        http_response_code(400);
        echo '不正なリクエストです(CSRF トークンエラー)。';
        exit;
    }

    // ここから先が本来の処理
}
PHP

ここでの重要ポイントは、比較に hash_equals() を使っていることです。CSRF トークンも「推測されたら困る値」なので、単純な === 比較ではなく、時間差が出にくい比較関数を使うのが望ましいです。


トークンの「使い捨て」にするかどうか

使い捨てにするパターン

より厳密にやるなら、「一度使ったトークンは無効にする」という設計もあります。例えば、フォーム送信が成功したら、そのトークンをセッションから削除し、次のフォーム表示時に新しいトークンを発行する、という形です。

function invalidate_csrf_token(): void
{
    unset($_SESSION['csrf_token']);
}
PHP

検証に成功したあとでこれを呼べば、「同じトークンを再利用した二重送信」を防ぎやすくなります。

ただし、実務では「セッションごとに 1 つのトークンを使い回す」だけでも、CSRF 対策としては十分なケースが多いです。まずはシンプルな形で確実に動かし、必要になったら使い捨てにする、という段階的な考え方で大丈夫です。


まとめ:今日からの「CSRF トークン生成」ユーティリティ

CSRF トークンの本質は、「フォーム表示時にサーバーとブラウザだけが共有する、推測されにくい秘密の値」です。そのためにやるべきことは、次の三つに集約されます。

暗号論的に安全な乱数(random_bytes())からトークンを生成する。
トークンをセッションに保存し、フォームに hidden フィールドとして埋め込む。
送信時にセッションのトークンとリクエストのトークンを hash_equals() で比較する。

ユーティリティとしてまとめると、こんなセットになります。

function generate_csrf_token(int $bytes = 32): string
{
    return bin2hex(random_bytes($bytes));
}

function issue_csrf_token(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = generate_csrf_token();
    }
    return $_SESSION['csrf_token'];
}

function validate_csrf_token(?string $tokenFromRequest): bool
{
    if (!isset($_SESSION['csrf_token'])) {
        return false;
    }

    if ($tokenFromRequest === null || $tokenFromRequest === '') {
        return false;
    }

    return hash_equals($_SESSION['csrf_token'], $tokenFromRequest);
}
PHP

もし、あなたのフォーム処理が「CSRF トークンなし」で動いているなら、そこがこのユーティリティを差し込むべき一番のポイントです。フォームの hidden に 1 行足し、受信側で 2〜3 行チェックを入れるだけで、アプリの“守りのレベル”が一段上がります。

PHPPHP
スポンサーリンク
シェアする
@lifehackerをフォローする
スポンサーリンク
タイトルとURLをコピーしました