From 712994b19634d41e7156bce9721c77ffa7ee2500 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Aug 2026 17:35:21 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01GAFKf86P78WqniPEP2b45J --- web/includes/session.php | 23 +++++++++++++++++++++++ web/index.php | 1 + 2 files changed, 24 insertions(+) diff --git a/web/includes/session.php b/web/includes/session.php index 9d99e0333..52a05011b 100644 --- a/web/includes/session.php +++ b/web/includes/session.php @@ -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(); diff --git a/web/index.php b/web/index.php index 2ab6b0925..9b5a9e8f9 100644 --- a/web/index.php +++ b/web/index.php @@ -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");