はじめに
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_session | bool | false | true にすると古いセッションファイルを削除する |
戻り値
- 成功時:
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更新や不審アクティビティ検出と組み合わせることで、より堅牢な認証基盤を構築できます。
