[PHP]session_regenerate_id()完全解説|セッション固定攻撃対策と安全なID再生成の実装方法

PHP

はじめに

Webアプリケーションのセキュリティにおいて、**セッション固定攻撃(Session Fixation Attack)**はログイン機能を持つあらゆるサイトが対象となる脅威です。攻撃者が事前に用意したセッションIDをユーザーに使わせ、そのIDでなりすまし認証を通過するという手口です。

session_regenerate_id() は、この攻撃を防ぐためにセッションIDを新しい値に再生成する関数です。ログイン・ログアウト・権限昇格のタイミングで呼び出すことで、セッションハイジャックのリスクを大幅に下げられます。PHPセキュリティの基本中の基本でありながら、実装が漏れがちな関数でもあります。


関数の概要

項目内容
関数名session_regenerate_id()
所属PHP セッション関数
導入バージョンPHP 4.3.2以降
PHP 8.x対応済み

構文

session_regenerate_id(bool $delete_old_session = false): bool

パラメータ

パラメータデフォルト説明
$delete_old_sessionboolfalsetrue にすると古いセッションファイルを削除する

戻り値

  • 成功時:true
  • 失敗時:false

⚠️ 注意session_regenerate_id()session_start() を呼び出したでなければ使用できません。セッションが開始されていない状態で呼ぶと警告が発生します。


$delete_old_session の使い分け

動作推奨シーン
false(デフォルト)古いセッションデータを保持する通信不安定・戻るボタン対策が必要な場合
true古いセッションファイルを削除するログイン・ログアウト時(セキュリティ優先)

ログイン処理では true を推奨します。古いIDが残ると攻撃者に悪用されるリスクがあるためです。


基本的な使い方

<?php
session_start();

// ログイン認証後にIDを再生成(古いセッションは削除)
if ($loginSuccess) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $userId;
    $_SESSION['role']    = 'member';
}

実践的なクラスベースの使用例

例1:ログイン処理でのセッション固定攻撃対策クラス

<?php
/**
 * ログイン時にセッション固定攻撃を防ぐクラス
 * 認証成功後に必ずセッションIDを再生成する
 */
class AuthSessionManager
{
    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_name('AppSession');
            session_start();
        }
    }

    /**
     * ログイン処理:認証成功後にセッションIDを再生成する
     */
    public function login(string $username, string $hashedPassword, array $userRecord): bool
    {
        if (!password_verify($hashedPassword, $userRecord['password_hash'])) {
            $this->recordFailedAttempt($username);
            return false;
        }

        // ① 古いセッションデータを引き継ぐ前にIDを再生成
        $oldId = session_id();
        $result = session_regenerate_id(true); // 古いセッションファイルを削除

        if (!$result) {
            throw new \RuntimeException('セッションIDの再生成に失敗しました');
        }

        // ② 新しいセッションにユーザー情報を格納
        $_SESSION['user_id']    = $userRecord['id'];
        $_SESSION['username']   = $userRecord['username'];
        $_SESSION['role']       = $userRecord['role'];
        $_SESSION['logged_in_at'] = time();
        $_SESSION['_token']     = bin2hex(random_bytes(32));

        echo "ログイン成功\n";
        echo "旧セッションID: {$oldId}\n";
        echo "新セッションID: " . session_id() . "\n";

        return true;
    }

    /**
     * ログアウト処理:セッションを完全に破棄する
     */
    public function logout(): void
    {
        // セッション変数をすべてクリア
        $_SESSION = [];

        // Cookieを削除
        if (ini_get('session.use_cookies')) {
            $params = session_get_cookie_params();
            setcookie(
                session_name(), '',
                time() - 42000,
                $params['path'],
                $params['domain'],
                $params['secure'],
                $params['httponly']
            );
        }

        session_destroy();
        echo "ログアウト完了\n";
    }

    private function recordFailedAttempt(string $username): void
    {
        $_SESSION['failed_attempts'] = ($_SESSION['failed_attempts'] ?? 0) + 1;
        echo "認証失敗: {$username} (試行回数: {$_SESSION['failed_attempts']})\n";
    }
}

/*
利用例:
$auth = new AuthSessionManager();
$auth->login('alice', $inputPassword, $userRecord);
*/

例2:権限昇格時のセッション再生成クラス

<?php
/**
 * 権限昇格(一般ユーザー → 管理者など)のタイミングで
 * セッションIDを再生成するクラス
 * sudo的な一時昇格にも対応する
 */
class PrivilegeEscalationGuard
{
    private array $roleHierarchy = [
        'guest'  => 0,
        'member' => 1,
        'editor' => 2,
        'admin'  => 3,
    ];

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * 権限を昇格させる(必ずセッションIDを再生成する)
     */
    public function escalateTo(string $newRole, string $reason = ''): bool
    {
        $currentRole  = $_SESSION['role'] ?? 'guest';
        $currentLevel = $this->roleHierarchy[$currentRole] ?? 0;
        $newLevel     = $this->roleHierarchy[$newRole] ?? 0;

        if ($newLevel <= $currentLevel) {
            echo "昇格不要またはダウングレード: {$currentRole} → {$newRole}\n";
            return false;
        }

        $oldId = session_id();

        // 権限昇格時は必ずセッションIDを再生成
        session_regenerate_id(true);

        $_SESSION['role']             = $newRole;
        $_SESSION['escalated_at']     = time();
        $_SESSION['escalation_reason'] = $reason;
        $_SESSION['previous_role']    = $currentRole;

        echo "権限昇格: {$currentRole} → {$newRole}\n";
        echo "理由: {$reason}\n";
        echo "旧ID: {$oldId}\n";
        echo "新ID: " . session_id() . "\n";

        return true;
    }

    /**
     * 昇格した権限を元に戻す
     */
    public function revoke(): void
    {
        $previousRole = $_SESSION['previous_role'] ?? 'member';
        session_regenerate_id(true);
        $_SESSION['role'] = $previousRole;
        unset($_SESSION['escalated_at'], $_SESSION['previous_role'], $_SESSION['escalation_reason']);
        echo "権限を {$previousRole} に戻しました\n";
    }
}

$guard = new PrivilegeEscalationGuard();
// $guard->escalateTo('admin', '管理者パネルへのアクセス');

例3:セッションハイジャック検出+自動再生成クラス

<?php
/**
 * セッションハイジャックの兆候を検出し、
 * 疑わしい場合はセッションIDを再生成するクラス
 */
class SessionHijackDetector
{
    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * リクエストのたびにフィンガープリントを検証する
     */
    public function verify(): bool
    {
        $currentFingerprint = $this->generateFingerprint();

        // 初回アクセス:フィンガープリントを記録
        if (!isset($_SESSION['_fingerprint'])) {
            $_SESSION['_fingerprint'] = $currentFingerprint;
            session_regenerate_id(false); // 初回は緩やかに再生成
            return true;
        }

        // フィンガープリントの変化を検出
        if (!hash_equals($_SESSION['_fingerprint'], $currentFingerprint)) {
            $this->handleSuspiciousActivity();
            return false;
        }

        // 定期的な予防的再生成(30分ごと)
        $this->periodicRegenerate();

        return true;
    }

    /**
     * UA・IPのハッシュ値でフィンガープリントを生成
     */
    private function generateFingerprint(): string
    {
        $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
        // IPアドレスは /24 のサブネット単位で比較(モバイルのIP変動に対応)
        $ipSegment = implode('.', array_slice(explode('.', $_SERVER['REMOTE_ADDR'] ?? ''), 0, 3));
        return hash('sha256', $ua . $ipSegment);
    }

    /**
     * 不審なアクティビティへの対応
     */
    private function handleSuspiciousActivity(): void
    {
        $oldId = session_id();
        // セッションを完全にリセット
        $userId = $_SESSION['user_id'] ?? null;
        session_unset();
        session_regenerate_id(true);

        $_SESSION['_fingerprint'] = $this->generateFingerprint();
        $_SESSION['_hijack_detected_at'] = time();

        echo "⚠️ セッションハイジャックの疑い\n";
        echo "旧ID: {$oldId} / 新ID: " . session_id() . "\n";
        echo "ユーザーID {$userId} を強制ログアウトしました\n";
    }

    /**
     * 30分ごとにセッションIDを予防的に再生成する
     */
    private function periodicRegenerate(): void
    {
        $interval = 1800; // 30分
        $lastRegen = $_SESSION['_last_regenerated'] ?? 0;

        if (time() - $lastRegen > $interval) {
            session_regenerate_id(false); // 古いデータは保持
            $_SESSION['_last_regenerated'] = time();
            echo "定期再生成: " . session_id() . "\n";
        }
    }
}

// $detector = new SessionHijackDetector();
// $detector->verify();

例4:二段階認証通過後のセッション昇格クラス

<?php
/**
 * 二段階認証(TOTP)通過後にセッションIDを再生成するクラス
 * 認証フローの各ステップでセッションIDを更新する
 */
class TwoFactorSessionUpgrader
{
    // 認証ステップの定義
    private const STEP_ANONYMOUS    = 0;
    private const STEP_PASSWORD_OK  = 1;
    private const STEP_2FA_OK       = 2;

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * ステップ1:パスワード認証通過
     */
    public function completePasswordStep(int $userId): void
    {
        session_regenerate_id(true); // IDを再生成

        $_SESSION['auth_step']  = self::STEP_PASSWORD_OK;
        $_SESSION['user_id']    = $userId;
        $_SESSION['step1_at']   = time();

        echo "ステップ1完了 (パスワードOK)\n";
        echo "新セッションID: " . session_id() . "\n";
    }

    /**
     * ステップ2:TOTPコード認証通過
     */
    public function complete2FAStep(string $inputCode): bool
    {
        if (($_SESSION['auth_step'] ?? 0) !== self::STEP_PASSWORD_OK) {
            echo "不正な順序でのアクセス\n";
            return false;
        }

        if (!$this->verifyTotp($inputCode)) {
            echo "TOTPコードが無効です\n";
            return false;
        }

        // 2FA通過でも再度IDを再生成
        session_regenerate_id(true);

        $_SESSION['auth_step']      = self::STEP_2FA_OK;
        $_SESSION['fully_authed_at'] = time();

        echo "ステップ2完了(2FA通過)- 完全認証済み\n";
        echo "最終セッションID: " . session_id() . "\n";

        return true;
    }

    public function isFullyAuthenticated(): bool
    {
        return ($_SESSION['auth_step'] ?? 0) === self::STEP_2FA_OK;
    }

    private function verifyTotp(string $code): bool
    {
        // 実際にはTOTPライブラリで検証する
        return strlen($code) === 6 && ctype_digit($code);
    }
}

/*
$upgrader = new TwoFactorSessionUpgrader();
$upgrader->completePasswordStep(42);
$upgrader->complete2FAStep('123456');
echo $upgrader->isFullyAuthenticated() ? "認証完了\n" : "未認証\n";
*/

例5:セッションIDのローリング再生成ミドルウェアクラス

<?php
/**
 * フレームワークのミドルウェアとして機能するセッション再生成クラス
 * 一定間隔でセッションIDを自動更新し、リプレイ攻撃を防ぐ
 *
 * ※ 分散環境(複数サーバー)での利用を考慮した実装例
 */
class RollingSessionMiddleware
{
    private int $regenerateInterval;
    private int $gracePeriod;

    /**
     * @param int $regenerateInterval 再生成間隔(秒)デフォルト15分
     * @param int $gracePeriod        旧IDを受け付ける猶予期間(秒)デフォルト30秒
     */
    public function __construct(int $regenerateInterval = 900, int $gracePeriod = 30)
    {
        $this->regenerateInterval = $regenerateInterval;
        $this->gracePeriod        = $gracePeriod;
    }

    public function handle(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        // 廃止予定IDでのアクセスを処理
        if ($this->isObsoleteSession()) {
            $this->forwardToNewSession();
            return;
        }

        // 再生成タイミングに達したか確認
        if ($this->shouldRegenerate()) {
            $this->rotate();
        }
    }

    private function shouldRegenerate(): bool
    {
        $lastTime = $_SESSION['_session_regenerated_at'] ?? 0;
        return (time() - $lastTime) > $this->regenerateInterval;
    }

    private function rotate(): void
    {
        $oldId = session_id();

        // 旧セッションに「廃止予定」マークと新IDを記録(猶予期間対応)
        $_SESSION['_obsolete']   = true;
        $_SESSION['_new_id']     = '';  // 再生成後に格納
        $_SESSION['_expires_at'] = time() + $this->gracePeriod;
        session_write_close();

        // 新セッションを開始
        session_start();
        session_regenerate_id(true);

        $newId = session_id();
        $_SESSION['_session_regenerated_at'] = time();

        echo "セッションID更新\n";
        echo "旧ID: {$oldId} → 新ID: {$newId}\n";
    }

    private function isObsoleteSession(): bool
    {
        return isset($_SESSION['_obsolete']) && $_SESSION['_obsolete'] === true;
    }

    private function forwardToNewSession(): void
    {
        if (time() > ($_SESSION['_expires_at'] ?? 0)) {
            // 猶予期間を超えた古いIDは無効化
            session_destroy();
            echo "セッション期限切れ: 再ログインが必要です\n";
        } else {
            echo "猶予期間中: 新セッションへ転送します\n";
        }
    }
}

// $middleware = new RollingSessionMiddleware(900, 30);
// $middleware->handle();

例6:セッション再生成ログ記録クラス

<?php
/**
 * セッションID再生成のたびにログを記録するクラス
 * セキュリティ監査や不審なアクティビティの追跡に使用する
 */
class AuditedSessionRegenerator
{
    private array $log = [];

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * 理由を記録しながらセッションIDを再生成する
     */
    public function regenerate(string $reason, bool $deleteOld = true): bool
    {
        $oldId    = session_id();
        $oldData  = $_SESSION;
        $result   = session_regenerate_id($deleteOld);
        $newId    = session_id();

        $entry = [
            'timestamp'  => date('Y-m-d H:i:s'),
            'reason'     => $reason,
            'old_id'     => substr($oldId, 0, 8) . '...',   // ログには一部のみ記録
            'new_id'     => substr($newId, 0, 8) . '...',
            'delete_old' => $deleteOld,
            'success'    => $result,
            'user_id'    => $oldData['user_id'] ?? null,
            'ip'         => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'ua_hash'    => substr(hash('sha256', $_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 16),
        ];

        $this->log[] = $entry;
        $this->persistLog($entry);

        return $result;
    }

    private function persistLog(array $entry): void
    {
        // 実際にはデータベースやログファイルへ書き込む
        $line = json_encode($entry, JSON_UNESCAPED_UNICODE);
        // file_put_contents('/var/log/app/session_audit.log', $line . "\n", FILE_APPEND);
        echo "[AUDIT] " . $line . "\n";
    }

    public function getLog(): array
    {
        return $this->log;
    }
}

$auditor = new AuditedSessionRegenerator();
$auditor->regenerate('ログイン成功', true);
$auditor->regenerate('管理者権限昇格', true);
$auditor->regenerate('定期ローテーション', false);

/*
出力例:
[AUDIT] {"timestamp":"2025-09-01 12:00:00","reason":"ログイン成功","old_id":"a3f8bc12...","new_id":"d92e44ab...","delete_old":true,"success":true,"user_id":null,"ip":"127.0.0.1","ua_hash":"3f8a2c1d4e7b0912"}
[AUDIT] {"timestamp":"2025-09-01 12:00:00","reason":"管理者権限昇格","old_id":"d92e44ab...","new_id":"7c14f3e9...","delete_old":true,"success":true,"user_id":null,"ip":"127.0.0.1","ua_hash":"3f8a2c1d4e7b0912"}
[AUDIT] {"timestamp":"2025-09-01 12:00:00","reason":"定期ローテーション","old_id":"7c14f3e9...","new_id":"b51a0c82...","delete_old":false,"success":true,"user_id":null,"ip":"127.0.0.1","ua_hash":"3f8a2c1d4e7b0912"}
*/

セッション固定攻撃(Session Fixation)とは

【攻撃の流れ】

① 攻撃者が自分のセッションID(例: ABC123)を取得する
② 攻撃者がそのIDをURLパラメータやCookieで被害者に踏ませる
③ 被害者がログインする(IDは ABC123 のまま)
④ 攻撃者が同じ ABC123 でアクセス → 被害者としてログイン済み状態になる

【対策】
ログイン成功直後に session_regenerate_id(true) を呼ぶ
→ IDが ABC123 から新しいランダム値に変わるため、攻撃者の手元のIDは無効になる

関連関数との比較

関数役割
session_regenerate_id()現在のセッションデータを引き継ぎながらIDだけを新しく生成
session_destroy()セッションデータをすべて破棄(ログアウト時)
session_unset()セッション変数をすべて削除(データのみ・IDは維持)
session_id()セッションIDの取得・手動設定
session_create_id()新しいセッションIDを生成するが適用はしない(PHP 7.1以降)

よくある落とし穴

<?php
// ❌ NG:session_start() の前に呼ぶと警告
session_regenerate_id(true);
session_start();

// ✅ OK:session_start() の後に呼ぶ
session_start();
session_regenerate_id(true);
<?php
// ❌ よくある実装漏れ:ログイン後に再生成を忘れる
session_start();
if ($loginSuccess) {
    $_SESSION['user_id'] = $userId; // IDが古いままでセッション固定攻撃に無防備
}

// ✅ 正しい実装:ログイン後に必ず再生成
session_start();
if ($loginSuccess) {
    session_regenerate_id(true); // ← これが必須
    $_SESSION['user_id'] = $userId;
}
<?php
// ❌ 注意:$delete_old_session = false のまま大量再生成するとファイルが溜まる
for ($i = 0; $i < 100; $i++) {
    session_regenerate_id(false); // 古いファイルが100個残る
}

// ✅ 古いセッションが不要なら true を指定する
session_regenerate_id(true);

まとめ

項目内容
関数名session_regenerate_id(bool $delete_old_session = false): bool
主な用途セッションIDの再生成(セッション固定攻撃対策)
呼び出しタイミングsession_start() の後
$delete_old_sessionログイン・ログアウト時は true 推奨
呼び出し必須タイミングログイン成功時 / ログアウト時 / 権限昇格時
戻り値成功 true / 失敗 false

session_regenerate_id() はセキュリティの観点で実装が必須の関数です。ログイン・ログアウト・権限昇格という「セッションの意味が変わる瞬間」には必ず呼び出すことを習慣にしましょう。定期的なID更新や不審アクティビティ検出と組み合わせることで、より堅牢な認証基盤を構築できます。


参考リンク

タイトルとURLをコピーしました