[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