[TikiWiki-commits] [Git][tikiwiki/tiki][master] [SEC] Harden Tiki remember-me cookies and invalidate on password change

"Espoir Baraka \(@esbarakabigega\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a84be33e1dfa_3818c34c73790@gitlab-sidekiq-low-urgency-cpu-bound-v2-789dc4448d-qg7tp.mail>

Espoir Baraka pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
cf1e13e6 by Espoir Baraka at 2026-08-18T22:02:11+02:00
[SEC] Harden Tiki remember-me cookies and invalidate on password change
---
* [SEC] Harden Tiki remember-me cookies and invalidate on password change

(cherry picked from commit f2aee8c8f0c67e8594547785cfecacee18752763)

See merge request tikiwiki/tiki!10962

- - - - -


6 changed files:

- lib/core/Realtime/SessionAwareApp.php
- lib/tikilib.php
- lib/userslib.php
- tiki-change_password.php
- tiki-login.php
- tiki-setup_base.php


Changes:

=====================================
lib/core/Realtime/SessionAwareApp.php
=====================================
@@ -83,10 +83,8 @@ class SessionAwareApp implements MessageComponentInterface
         }
 
 
-        $cookie_site = preg_replace("/[^a-zA-Z0-9]/", "", $prefs['cookie_name']);
-        $user_cookie_site = 'tiki-user-' . $cookie_site;
         $user = $_SESSION['u_info']['login'];
-        $_SESSION["$user_cookie_site"] = $user;
+        $_SESSION[\TikiLib::lib('user')->getRememberMeCookieName()] = $user;
 
         $tikilib->setSessionId(session_id());
 


=====================================
lib/tikilib.php
=====================================
@@ -147,6 +147,56 @@ class TikiLib extends TikiDb_Bridge
             : $encoded_value;
     }
 
+    /**
+     * Secure, HttpOnly and SameSite flags aligned with the PHP session cookie parameters.
+     * Shared by remember-me handling and auxiliary cookies such as the session validation cookie.
+     *
+     * @return array
+     */
+    public function getSetcookieSecurityAttributesFromSession()
+    {
+        $session_params = session_get_cookie_params();
+        $secure = ! empty($session_params['secure'])
+            || (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+            || (! empty($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https');
+
+        $samesite = trim((string)($session_params['samesite'] ?? '')) ?: 'Lax';
+        if (strcasecmp($samesite, 'None') === 0) {
+            $samesite = 'None';
+            $secure = true;
+        } elseif (strcasecmp($samesite, 'Strict') === 0) {
+            $samesite = 'Strict';
+        } elseif (strcasecmp($samesite, 'Lax') === 0) {
+            $samesite = 'Lax';
+        }
+
+        $httponly = isset($session_params['httponly']) ? (bool)$session_params['httponly'] : true;
+
+        return [
+            'secure' => $secure,
+            'httponly' => $httponly,
+            'samesite' => $samesite,
+        ];
+    }
+
+    /**
+     * Set a cookie with explicit path and domain plus session-aligned Secure, HttpOnly and SameSite.
+     * Used for the extra session validation cookie in tiki-setup_base.
+     *
+     * @param int $expires Unix timestamp
+     */
+    public function setCookieWithSessionSecurityAttributes($name, $value, $expires, $path, $domain)
+    {
+        setcookie($name, $value, array_merge(
+            [
+                'expires' => $expires,
+                'path' => $path,
+                'domain' => $domain,
+            ],
+            $this->getSetcookieSecurityAttributesFromSession()
+        ));
+    }
+
     // DB param left for interface compatibility, although not considered
     /**
      * @param null $db


=====================================
lib/userslib.php
=====================================
@@ -269,6 +269,76 @@ class UsersLib extends TikiLib
         return in_array($group, $this->list_all_groups());
     }
 
+    /**
+     * Remember-me cookie name and session key, derived from the cookie_name preference.
+     * Bootstrap sets global user_cookie_site to this value once for the whole request.
+     */
+    public function getRememberMeCookieName()
+    {
+        global $prefs;
+
+        $cookie_site = preg_replace("/[^a-zA-Z0-9]/", "", $prefs['cookie_name']);
+        return 'tiki-user-' . $cookie_site;
+    }
+
+    /**
+     * Remember-me setcookie options: path and domain from prefs, Secure and HttpOnly and SameSite from TikiLib.
+     *
+     * @param int $expires Unix timestamp
+     * @return array
+     */
+    public function getRememberMeCookieOptions($expires)
+    {
+        global $prefs;
+
+        $tikilib = TikiLib::lib('tiki');
+        return array_merge(
+            [
+                'expires' => $expires,
+                'path' => $prefs['feature_intertiki_sharedcookie'] == 'y' ? '/' : $prefs['cookie_path'],
+                'domain' => $prefs['cookie_domain'],
+            ],
+            $tikilib->getSetcookieSecurityAttributesFromSession()
+        );
+    }
+
+    /**
+     * Remember me on login: create the stored token and set the browser cookie.
+     */
+    public function issueRememberMeCookieForUserId($userId)
+    {
+        global $prefs;
+
+        $tikilib = TikiLib::lib('tiki');
+        $secret = $this->create_user_cookie($userId);
+        $value = $secret . '.' . $userId;
+        setcookie($this->getRememberMeCookieName(), $value, $this->getRememberMeCookieOptions($tikilib->now + $prefs['remembertime']));
+    }
+
+    /**
+     * Refresh remember-me expiry from the incoming cookie value and user id.
+     */
+    public function refreshRememberMeCookieFromLoginValue($loginCookieValue, $userId)
+    {
+        global $prefs;
+
+        $cookie_parts = explode('.', $loginCookieValue, 2);
+        if (count($cookie_parts) < 2) {
+            return;
+        }
+        $secret = array_shift($cookie_parts);
+        $secret = $this->create_user_cookie((int)$userId, $secret);
+        $value = $secret . '.' . $userId;
+        $tikilib = TikiLib::lib('tiki');
+        setcookie($this->getRememberMeCookieName(), $value, $this->getRememberMeCookieOptions($tikilib->now + $prefs['remembertime']));
+    }
+
+    /** Remove the remember-me cookie using the same options as issue and refresh. */
+    public function clearRememberMeCookieForSite()
+    {
+        setcookie($this->getRememberMeCookieName(), '', $this->getRememberMeCookieOptions(time() - 3600));
+    }
+
     /**
      * @param string $user : username
      * @param bool $remote_logout : logged out remotely (so do not redirect)
@@ -277,7 +347,9 @@ class UsersLib extends TikiLib
      */
     public function user_logout($user, $remote_logout = false, $redir = '')
     {
-        global $prefs, $user_cookie_site;
+        global $prefs;
+
+        $rememberMeCookieName = $this->getRememberMeCookieName();
 
         $logslib = TikiLib::lib('logs');
         $logslib->add_log('login', 'logged out');
@@ -285,8 +357,8 @@ class UsersLib extends TikiLib
         $userInfo = $this->get_user_info($user);
         if ($prefs['login_multiple_forbidden'] === 'y') {
             $this->delete_user_cookie($userInfo['userId']);
-        } elseif (! empty($_COOKIE[$user_cookie_site])) {
-            $secret = explode('.', $_COOKIE[$user_cookie_site]);
+        } elseif (! empty($_COOKIE[$rememberMeCookieName])) {
+            $secret = explode('.', $_COOKIE[$rememberMeCookieName]);
             $this->delete_user_cookie($userInfo['userId'], $secret[0]);
         }
 
@@ -332,7 +404,7 @@ class UsersLib extends TikiLib
             }
         }
 
-        setcookie($user_cookie_site, '', -3600, $prefs['feature_intertiki_sharedcookie'] == 'y' ? '/' : $prefs['cookie_path'], $prefs['cookie_domain']);
+        $this->clearRememberMeCookieForSite();
 
         /* change group home page or deactivate if no page is set */
         if (! empty($redir)) {
@@ -351,7 +423,7 @@ class UsersLib extends TikiLib
             $url .= '?' . SID;
         }
 
-        unset($_SESSION['cas_validation_time'], $_SESSION[$user_cookie_site], $_SESSION['phpCAS']);
+        unset($_SESSION['cas_validation_time'], $_SESSION[$rememberMeCookieName], $_SESSION['phpCAS']);
         if (session_status() !== PHP_SESSION_NONE) {
             if (ini_get('session.use_cookies')) {
                 $params = session_get_cookie_params();
@@ -853,9 +925,7 @@ class UsersLib extends TikiLib
                     $username = $saml_username;
                 }
 
-                $cookie_site = preg_replace("/[^a-zA-Z0-9]/", "", $prefs['cookie_name']);
-                $user_cookie_site = 'tiki-user-' . $cookie_site;
-                $_SESSION["$user_cookie_site"] = $username;
+                $_SESSION[$this->getRememberMeCookieName()] = $username;
 
                 $randompass = $this->genPass();
                 if (! $userTikiPresent) {


=====================================
tiki-change_password.php
=====================================
@@ -206,6 +206,14 @@ if (isset($_REQUEST["change"]) && $access->checkCsrf()) {
 
             // One-time marker: do not allow reuse of the validation session for another change
             unset($_SESSION['pending_new_user_password'], $_SESSION['last_validation']);
+            if ($res) {
+                // Invalidate remember-me cookies after password change
+                $userInfo = $userlib->get_user_info($user);
+                if (! empty($userInfo['userId'])) {
+                    $userlib->delete_user_cookie((int)$userInfo['userId']);
+                }
+                $userlib->clearRememberMeCookieForSite();
+            }
 
             // Mark reset token as used only after successful password change
             if (! empty($secure_token) && ! $server_new_user_validation && ! $must_change_password) {


=====================================
tiki-login.php
=====================================
@@ -612,9 +612,7 @@ if ($isvalid && ($isOpenIdValid || $access->checkCsrf(null, null, null, null, nu
             // Now if the remember me feature is on and the user checked the rememberme checkbox then ...
             if ($prefs['rememberme'] == 'always' || $prefs['rememberme'] != 'disabled' && isset($_REQUEST['rme']) && $_REQUEST['rme'] == 'on') {
                 $userInfo = $userlib->get_user_info($user);
-                $userId = $userInfo['userId'];
-                $secret = $userlib->create_user_cookie($userId);
-                setcookie($user_cookie_site, $secret . '.' . $userId, $tikilib->now + $prefs['remembertime'], $prefs['feature_intertiki_sharedcookie'] == 'y' ? '/' : $prefs['cookie_path'], $prefs['cookie_domain']);
+                $userlib->issueRememberMeCookieForUserId((int)$userInfo['userId']);
                 $logslib->add_log('login', 'got a cookie for ' . $prefs['remembertime'] . ' seconds');
             }
         }


=====================================
tiki-setup_base.php
=====================================
@@ -302,14 +302,13 @@ if (isset($_SERVER["REQUEST_URI"]) && ! str_contains($_SERVER['REQUEST_URI'], 't
                 } else {
                     $sequence = $tikilib->generate_unique_sequence(16, true);
                     $_SESSION['extra_validation'] = $sequence;
-                    setcookie($extra_cookie_name, $sequence, [
-                        'expires' => time() + 365 * 24 * 3600,
-                        'path' => $session_params['path'],
-                        'domain' => $session_params['domain'],
-                        'secure' => $session_params['secure'],
-                        'httponly' => $session_params['httponly'],
-                        'samesite' => $session_params['samesite'],
-                    ]);
+                    $tikilib->setCookieWithSessionSecurityAttributes(
+                        $extra_cookie_name,
+                        $sequence,
+                        time() + 365 * 24 * 3600,
+                        $session_params['path'],
+                        $session_params['domain']
+                    );
                     unset($sequence);
                 }
             }
@@ -344,7 +343,9 @@ $maxRecords = $prefs['maxRecords'];
 $smarty->assign('maxRecords', $maxRecords);
 
 $userlib = TikiLib::lib('user');
-$user = null;  //We are still in the global scope, this variable will be available everywhere as a global.  This assignment is here so IDEs will find it.
+// Remember-me cookie name comes only from UsersLib getRememberMeCookieName so it cannot drift.
+$user_cookie_site = $userlib->getRememberMeCookieName();
+$user = null;  // Global scope for IDEs and included scripts.
 require_once('lib/breadcrumblib.php');
 // ------------------------------------------------------
 // DEAL WITH XSS-TYPE ATTACKS AND OTHER REQUEST ISSUES
@@ -530,10 +531,7 @@ if (TIKI_API) {
         $user = null;
     }
 } else {
-    // in the case of tikis on same domain we have to distinguish the realm
-    // changed cookie and session variable name by a name made with browsertitle
-    $cookie_site = preg_replace("/[^a-zA-Z0-9]/", "", $prefs['cookie_name']);
-    $user_cookie_site = 'tiki-user-' . $cookie_site;
+    // Same domain, several Tikis: login cookie realm. user_cookie_site is set once after prefs load.
     $login_cookie_value = $_COOKIE["$user_cookie_site"] ?? '';
     // if remember me is enabled, check for cookie where auth hash is stored
     // user gets logged in as the first user in the db with a matching hash
@@ -560,10 +558,7 @@ if (TIKI_API) {
                 if (empty($userId)) {    // for intertiki
                     $userId = $userlib->get_user_id($user);
                 }
-                $cookie_parts = explode('.', $login_cookie_value, 2);
-                $secret = array_shift($cookie_parts);
-                $secret = $userlib->create_user_cookie($userId, $secret);
-                setcookie($user_cookie_site, $secret . '.' . $userId, $tikilib->now + $prefs['remembertime'], $prefs['feature_intertiki_sharedcookie'] == 'y' ? '/' : $prefs['cookie_path'], $prefs['cookie_domain']);
+                $userlib->refreshRememberMeCookieFromLoginValue($login_cookie_value, (int)$userId);
                 $logslib->add_log('login', 'refreshed a cookie for ' . $prefs['remembertime'] . ' seconds');
             }
         }



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/cf1e13e61c246b2a57cc04651dbce5cfa293be3c

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/cf1e13e61c246b2a57cc04651dbce5cfa293be3c
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.