Skip to content

Sessions: validate ids, opt-in query parameter, expiry, regenerate() - #14

Merged
daedeloth merged 1 commit into
masterfrom
fix/session-hardening
Oct 7, 2026
Merged

daedeloth merged 1 commit into
masterfrom
fix/session-hardening

Conversation

@daedeloth

Copy link
Copy Markdown
Member

What changes

Session id validation

  • SessionHandler::isValidSessionId() accepts ids in the format PHP generates: 22 to 256 characters out of A-Za-z0-9,-.
  • An id from the application (Session::connect($id)), the session cookie or the query parameter that is not in that format is ignored; the request gets a new session.
  • SessionHandler and DbSessionHandler implement SessionUpdateTimestampHandlerInterface (validateId, updateTimestamp), and start() switches on session.use_strict_mode. An id is only kept when it has a stored session; otherwise PHP generates a new one.
    • DbSessionHandler checks for a row that has not expired (the same lookup read() uses, one query for both).
    • SessionHandler wraps PHP's own save handler and can only check for a stored session when that handler is files; with another save handler only the format is checked.
  • updateTimestamp() writes the session, so a session without data is still stored on its first request, as before.

Query parameter is opt-in

  • The PSID query parameter is only read after \Neuron\SessionHandlers\SessionHandler::setQueryParameterEnabled (true);. It is off by default. The cookie still takes precedence.

Logging

  • The log line for a starting or closing session names the source of the id (application, cookie, query parameter, new) and no longer contains the id.

Expiry (DbSessionHandler)

  • read() treats a row whose set_time is older than session.gc_maxlifetime as absent and removes it.
  • gc($maxlifetime) deletes rows older than the lifetime it is given, instead of a fixed 10 minutes.

New method

  • Session::regenerate() calls session_regenerate_id(true) when a session is active (and the cookie can still be sent) and returns whether the id changed. The session data is kept. Meant for login code.

Compatibility

  • Applications that pass a session id in the PSID query parameter must call SessionHandler::setQueryParameterEnabled (true) before the session starts. Without it the parameter is ignored and such a request starts a new, empty session.
  • An id given to Session::connect($id) must now belong to a stored session; it can no longer be used to create a session with a chosen id.
  • Session lifetime with DbSessionHandler is now exactly session.gc_maxlifetime seconds since the last request (PHP default: 1440). Before, a row was removed by garbage collection once it was 10 minutes old, whenever garbage collection happened to run. Applications that want a specific lifetime should set session.gc_maxlifetime before the session starts.
  • session.use_strict_mode is switched on by SessionHandler::start().
  • No schema change. Queries are unchanged apart from read() also selecting set_time.

Tests

tests/SessionHandlerTest.php, tests/DbSessionHandlerTest.php and tests/SessionTest.php cover the id format, the source resolution and the opt-in switch, the log messages, the expiry decision and the queries against a stub database. They do not start a session or send headers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE

- A request may only bring a session id in the format PHP generates
  that has a stored session. The handlers implement
  SessionUpdateTimestampHandlerInterface and switch on
  session.use_strict_mode when a session starts.
- The PSID query parameter is only read after
  SessionHandler::setQueryParameterEnabled (true). Off by default.
- The log names where a session id came from, not the id.
- DbSessionHandler treats a row older than session.gc_maxlifetime as
  absent and removes it; gc() uses the lifetime it is given.
- Session::regenerate () gives the active session a new id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE
@daedeloth
daedeloth merged commit 0af7598 into master Oct 7, 2026
5 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.

1 participant