PHPのsession_regeneration_id とセッションフィクシエーション対策

CSRF 対策のうち、セッションフィクセーションを避けるために使われる
session_regenerate_id() 関数について、ITProで間違い(になる場合がある)記事を見つけたのでメモ。
 
関数の知られざる引数(print_r関数、session_regenerate_id関数)[より

同じように、PHP 5.1からはsession_regenerate_id関数にも第1引数が指定されました。第1引数をtrueに指定すると、古くなったセッションは削除されるようになります。
 
もともとセッション固定攻撃を防ぐために用意された関数ではなく、PHP 5.1 までの同関数には古いセッションファイルを削除する機能が無かったため、セッション固定攻撃への対策としては不十分でした。
 
今後は、session_regenerate_id関数の引数に必ずtrueを指定することを忘れないようにしましょう。また、それまでのPHPで使う場合は、以下のコードを使うことで、セッション固定攻撃を防ぐことができます。

のように、session_regenerate_id() の第一引数に true を常に指定すべきと書かれていますが、session_regenerate_id(true) はむやみに使ってはなりません。
 
ショッピングカートなど限定的な用途でセッションを使っている場合は問題ないのですが、常にログインが必要なサイトなどで非同期で複数のリクエストを実行する可能性がある場合、古いセッションが削除されると問題になりことがあります。
 
たとえば同じ HTML 内に認証が必要な画像などの埋め込みコンテンツが複数あったり AJAX で認証が必要なデータにアクセスする場合、一つのセッションIDで複数のリクエストを行うため、2リクエスト目以降も古いセッションで認証することになるからです。
 
代替案として、session_regenerate_id(false) と session_write_close() を組み合わせて古いセッションのみ有効期間を設定してやるとよい、とsession_regenrate_idのコメント欄に書かれていました。
 
実装例:

// ユーザがリクエストしたセッションIDからセッション情報をロードする。
session_start();
// —————————————————–
// 以下セッションフィクセーション対策
if(isset($_SESSION[‘expire’]) && $_SESSION[‘expire’] < time()){
    // 過去に session_regenerate_id() で置き換えられた古いセッションによるアクセス。
    // 有効期限を過ぎていればセッション情報をクリアする。
    $_SESSION = array();
}
 
// 古いセッションは2分間で消えるように設定。
$_SESSION[‘expire’] = time() + (2*60);
// 有効期限を記録するため、一旦明示的にセッションを閉じ、$_SESSION の中身を古いセッションIDで保存する。
session_write_close();
 
// 閉じたセッションを開きなおす。
session_start();
// セッションIDを更新し、$_SESSION を古いセッションIDから切り離す。
session_regenerate_id();
// 新しいセッションID は有効期限なし。
unset($_SESSION[‘expire’]);
// ——————————————-
// セッションフィクセーション対策ここまで。
// あとは通常通り。
$_SESSION[‘user_id’] = “1234”;

 
また、session_regenerate_id() だけではセッション所有者のリクエストによる XSRF は防ぐことはできないため、こちら(開発者のための正しいCSRF対策)を参考にワンタイムトークン等も適宜組み合わせましょう。