Skip to content

fix(session): migrate all per-key state on reGenerateID() - #423

Merged
usernane merged 1 commit into
mainfrom
dev
Sep 27, 2026
Merged

usernane merged 1 commit into
mainfrom
dev

Conversation

@usernane

Copy link
Copy Markdown
Member

Summary

Migrate the full per-key session state to the new ID inside reGenerateID() so regenerating the session ID during a request no longer orphans data.

Motivation

reGenerateID() only changed the session cookie value; it did not migrate any per-key rows to the new ID. In the standard session-fixation login flow — reGenerateID() then setUser() without a close() before the request ends (the start-session middleware's afterSend() only calls validateStorage()) — the new ID ended up with only the eagerly-written _user key, while _meta, the user's companion state and all previously stored application keys stayed under the old ID. On the next request, start() reads _meta for the new ID, finds null, treats it as a brand-new session and loses the authenticated user and all session data. Fixes #422 (follow-up to the #418 user-persistence fix).

Changes

  • Session::reGenerateID() now migrates the entire session state to the new ID:
    1. Snapshot all rows under the old ID via readAll(), decrypting each value with the old ID (_meta is a plain array and is copied as-is).
    2. Switch the cookie to the new ID.
    3. Re-write every key under the new ID, re-encrypting with the new ID (encryption is keyed by session ID, so raw values can't be copied directly).
    4. destroy() the old ID's rows so no orphaned/duplicate state remains.

UI Changes

Not applicable.

How to Test / Verify

New regression test in tests/WebFiori/Framework/Tests/Session/SessionTest.php:

  • testSessionStateSurvivesRegenerateIdWithoutClose — sets custom keys (csrf, cart, lang_pref), runs reGenerateID() then setUser() with no close(), resumes the new ID, and asserts the user AND all custom keys survive, and that getVars() excludes reserved keys.

Verified the related session tests together (user persistence, reserved-key guards, and this regression): 4 tests / 32 assertions passing. The regression test fails on the pre-fix code and passes after.

Breaking Changes and Migration Steps

None. This only changes internal behavior of reGenerateID() to preserve state that previously would have been lost.

Checklist

  • I reviewed my own diff before requesting review
  • My commits follow Conventional Commits
  • I added/updated tests (or explained why not)
  • I updated docs (if needed) Docs Repo
  • I considered backward compatibility
  • I considered security

Related issues

Closes #422

reGenerateID() only changed the session cookie value and left every
per-key row (application keys, _meta, _user) orphaned under the old ID.
In the session-fixation login flow (reGenerateID() then setUser() with
no close()), the new ID ended up with only _user; on the next request
start() saw no _meta, treated the new ID as brand-new, and lost the user
and all session data.

reGenerateID() now migrates the full session state to the new ID:
snapshot all rows under the old ID (decrypting with the old ID; _meta is
a plain array copied as-is), switch the cookie to the new ID, re-write
every key re-encrypted under the new ID (encryption is keyed by session
ID), then destroy the old ID's rows.

Adds a regression test covering the user and multiple custom keys
surviving reGenerateID() without an intervening close().
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Sep 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.94%. Comparing base (1517dd3) to head (971c662).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #423      +/-   ##
============================================
+ Coverage     81.75%   81.94%   +0.19%     
- Complexity     3288     3293       +5     
============================================
  Files           106      106              
  Lines          9721     9735      +14     
============================================
+ Hits           7947     7977      +30     
+ Misses         1774     1758      -16     
Flag Coverage Δ
php-8.1 80.80% <100.00%> (+0.22%) ⬆️
php-8.2 80.80% <100.00%> (+0.22%) ⬆️
php-8.3 89.18% <100.00%> (+0.22%) ⬆️
php-8.4 89.18% <100.00%> (+0.22%) ⬆️
php-8.5 89.07% <100.00%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@usernane
usernane merged commit 19f9b5a into main Sep 27, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(session): user lost after reGenerateID() — _user persisted for new id but _meta is not, so start() skips restoreUser()

1 participant