レガシーPHPリファクタ 13:IP・UAベースの検証から、CSRFトークンへ

概要

今回は、セキュリティおよびセッション制御の要である「IP/UAベースの検証から、CSRFトークンへの置き換え」について解説します。

『Smarty動的Webサイト構築入門』第4.7章(アンケートフォームサンプル)のセキュリティおよびセッション制御の実装を、現代のWebアプリケーション標準に合わせてリファクタリングします。


背景・変更の理由

1. 当時の設計思想(2007年当時)

執筆当時、Webの主流は http:// であり、ネットワーク上のパケット盗聴やクロスサイトスクリプティング(XSS)によるCookie漏洩が日常的な脅威でした。そのため、当時のサンプルコードでは、セッションの乗っ取り(セッションハイジャック)を防ぐ目的で、セッション開始時のIPアドレスとUser-Agentをハッシュ化してトークン生成し、検証していました。

(※ガラケー時代の補足) 当時のガラケー向けサイトでは、IP/UAハッシュ検証ではなく、通信キャリアごとの「端末識別番号」をベースにログイン・セッション管理を行うのが一般的でした。

2. 現代における課題

しかし、時代とともにインターネットを取り巻く環境は大きく変化しました。

  • Web全体の https:// 普及により、通信経路の盗聴リスクが激減したこと。
  • スマートフォンの普及に伴い、ユーザーのIPアドレスが頻繁に変動するようになったこと。

結果として、IP/UAハッシュ検証は、現代のスマホ環境においては、正常なユーザーのセッションが頻繁にリセットされ、ログアウトさせられてしまうという実害を招きます。


主な変更内容

今回のリファクタリングでは、不安定な環境要因に依存した検証を廃止し、標準的なCSRF対策へと刷新しました。

1. IP/UAハッシュ検証の廃止

モバイル回線でのログアウトを防ぐため、不安定な REMOTE_ADDR と HTTP_USER_AGENT に依存した検証を撤廃しました。

2. ステートフルなCSRFトークンの導入

  • 暗号学的に安全な疑似乱数生成器(random_bytes(32))を用いて、セッションごとに一意のCSRFトークンを発行します。
  • 検証には、タイミング攻撃を防ぐ hash_equals() を採用しています。

3. トラフィック制御とライフサイクル

  • 入力画面(index または form)でトークンを保持し、確認画面(confirm)を経て、送信完了(submit)時にトークンを検証・破棄します。

実装差分

修正前:IP/UAハッシュによる独自検証

入力フォーム・確認ページ・登録処理において、hidden でトークンを仕込まず、サーバー側でIPとUAを結合したハッシュをセッションと比較していました。

$token = md5($config['token_salt'] . $_SERVER['HTTP_USER_AGENT'] . $_SERVER['REMOTE_ADDR']);
if (!isset($_SESSION['token']) || $_SESSION['token'] != $token) {
    session_regenerate_id();
    $_SESSION = [];
    $_SESSION['token'] = $token;
}

修正後:標準的なCSRFトークンによる検証

1. 入力フォーム側(トークンの生成)

/**
 * CSRFトークンを取得(存在しない場合は生成してセッションに保存)
 */
function getCsrfToken(string $appId): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

テンプレート側では、隠しフィールドとして埋め込みます。

<input type="hidden" name="csrf_token" value="{$csrf_token}">

2. 確認ページ・登録処理側(トークンの検証)

/**
 * CSRFトークンを検証
 * csrf_token[]=x の形式も考慮して mixed とします
 */
function validateCsrfToken(string $appId, mixed $tokenFromPost): bool
{
    $storedToken = $_SESSION['csrf_token'] ?? '';
    if (!is_string($tokenFromPost) || empty($storedToken) || empty($tokenFromPost)) {
        return false;
    }
    return hash_equals($storedToken, $tokenFromPost);
}

if (!validateCsrfToken($appId, $postVars['csrf_token'] ?? '')) {
    header('HTTP/1.1 400 Bad Request');
    die('不正なアクセスです。(CSRFトークン検証エラー)');
}

テスト運用のこぼれ話:E2Eテストの必要性

今回の修正に伴い、Golden Master テストでの POST送信の挙動が「不正なアクセスです。」になってしまいました。直前のフォームの CSRFトークンを使って POST送信する必要があるからです。

クロールベースの Golden Master では CSRFトークン周りのシミュレーションが難しくなります。

実運用を見据えると、Playwright などの本格的な E2E テスト環境の導入が必要と考えます。


【コラム】ミリ秒単位の二重送信(スマホの裏リトライ)に関する補足

CSRFトークンは「正当な送信元からのリクエストか」を検証するためのものですが、スマホの電波が不安定なときに発生する裏側での自動リトライなど、ミリ秒単位で同時並行するリクエスト(競合状態)の完全なブロックは、アプリケーション層(セッション管理)だけでは困難です。

実務の堅牢なシステム開発においては、本サンプルコードのようなセッション制御に加え、次のような多層防御が重要になります。

  • データベース層での一意制約(Unique Constraint)の設定
  • 排他制御(SELECT ... FOR UPDATE など)の活用

フォームの二重送信対策は、単一のレイヤーだけでなく、データベースを含めた全体のライフサイクルで設計することが肝要です。


今回のリファクタにより、レガシー特有の「スマホで動かない不具合」が解消され、現代のWeb標準にふさわしい堅牢なフォーム処理へと生まれ変わりました。

参考PR

refactor(chapter4_7): IP/UAベースの検証から、モダンなCSRFトークン制御へ刷新 by ninton · Pull Request #664 · ninton/smartybook
概要『Smarty動的Webサイト構築入門』第4.7章(アンケートフォームサンプル)のセキュリティおよびセッション制御の実装を、現代のWebアプリケーション標準に合わせてリファクタリングします。背景・変更の理由1. 当時の設計思想(2007...
レガシーPHPリファクタ 13:IP・UAベースの検証から、CSRFトークンへ
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.
タイトルとURLをコピーしました