fix: only store sessions for clients that carry them

Every request through index.php starts a session and always dirties it:
zm_session_set_remote_addr() writes remoteAddr, and index.php stores skin,
css and navbar_type. ZMSessionHandler::write() then persisted that session
unconditionally, so any request arriving without a ZMSESSID cookie left a
Sessions row behind that nothing would ever load again.

Viewing an event polls the event's server every ZM_WEB_REFRESH_STATUS
seconds via monitorUrl, which is absolute when the monitor has a Server
row. Those cross-origin ajax polls carry auth in the URL and no cookie, so
each one added a Sessions row every few seconds. Bot scans of the login
page did the same.

Persist a session only when the client presented our cookie, or when
zm_session_persist() marks it as one we are issuing: login, and the
postLoginQuery stashed before redirecting to the login page.

Verified on a live install by logging row counts from the save handler:
three cookieless requests skipped the write and left the count unchanged,
while a cookie-jar run wrote on the request that returned the cookie.
php -l clean on both files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFKf86P78WqniPEP2b45J
This commit is contained in:
Isaac ConnorandClaude Opus 5 committed 2026-08-25 17:36:31 -04:00
1 parent 030a56f8db
commit 712994b196
2 files changed
+24

No files matched your search

+23
View File
@@ -40,6 +40,26 @@ function zm_setcookie($cookie, $value, $options=array()) {
//ZM\Debug("Setting cookie for $cookie to $value");
}
// A session is only worth storing if the client actually carries it. A request
// that arrives without our cookie - a bot, an image tag or a cross-origin ajax
// poll authenticated by auth hash or token - still gets a session for the life
// of the request, but writing it out leaves a Sessions row that nothing will
// ever load again. Viewing an event polls the event's server every
// ZM_WEB_REFRESH_STATUS seconds, so a cross-origin poll used to add a row every
// few seconds. Login is the exception: that is where a session is first issued.
$zm_session_persist = false;
// Store this session even though the client arrived without our cookie.
function zm_session_persist() {
global $zm_session_persist;
$zm_session_persist = true;
}
function zm_session_is_persistable() {
global $zm_session_persist;
return $zm_session_persist || !empty($_COOKIE[session_name()]);
}
// ZM session start function support timestamp management
function zm_session_start() {
if (ini_get('session.name') != 'ZMSESSID') {
@@ -115,6 +135,8 @@ function zm_session_regenerate_id() {
// Assumes zm_session_start() has been called previously.
function zm_session_regenerate_id_login() {
if (!is_session_started()) zm_session_start();
// The client has no cookie yet on a first login, but this session must be stored.
zm_session_persist();
// Discard any pre-auth session contents so nothing carries across the
// authentication boundary.
$_SESSION = array();
@@ -190,6 +212,7 @@ class ZMSessionHandler implements SessionHandlerInterface {
return '';
}
public function write($id, $data) : bool {
if (!zm_session_is_persistable()) return true;
if (!($db = zmDbConnOrNull())) return false;
// Create time stamp
$access = time();
+1
View File
@@ -263,6 +263,7 @@ if ( ZM_OPT_USE_AUTH and (!isset($user) or !($user instanceof ZM\User)) and ($vi
$postLoginQuery = $_SERVER['QUERY_STRING'];
$redirect = '?view=login'.($postLoginQuery?'&postLoginQuery=' . urlencode($postLoginQuery):'');
zm_session_start();
zm_session_persist(); // must survive the redirect even if the client had no cookie
$_SESSION['postLoginQuery'] = $postLoginQuery;
session_write_close();
ZM\Debug("Redirecting to $redirect");