Remove session keys, and always set httpOnly

This commit is contained in:
Kijin Sung 2023-07-22 19:53:51 +09:00
parent c53e0a93f5
commit 60a3edc994
2 changed files with 41 additions and 171 deletions

View file

@ -1032,7 +1032,7 @@ class ModuleHandler extends Handler
// Handle redirects. // Handle redirects.
if($oModule->getRedirectUrl()) if($oModule->getRedirectUrl())
{ {
if ($_SESSION['is_new_session']) if (!empty($_SESSION['is_new_session']))
{ {
ob_end_clean(); ob_end_clean();
echo sprintf('<html><head><meta charset="UTF-8" /><meta http-equiv="refresh" content="0; url=%s" /></head><body></body></html>', escape($oModule->getRedirectUrl())); echo sprintf('<html><head><meta charset="UTF-8" /><meta http-equiv="refresh" content="0; url=%s" /></head><body></body></html>', escape($oModule->getRedirectUrl()));

View file

@ -81,7 +81,7 @@ class Session
ini_set('session.use_strict_mode', 1); ini_set('session.use_strict_mode', 1);
if ($samesite) if ($samesite)
{ {
if (version_compare(PHP_VERSION, '7.3', '>=')) if (PHP_VERSION_ID >= 70300)
{ {
ini_set('session.cookie_samesite', $samesite); ini_set('session.cookie_samesite', $samesite);
} }
@ -90,7 +90,7 @@ class Session
$path = ($path ?: '/') . '; SameSite=' . $samesite; $path = ($path ?: '/') . '; SameSite=' . $samesite;
} }
} }
session_set_cookie_params($lifetime, $path, $domain, $secure, $secure); session_set_cookie_params($lifetime, $path, $domain, $secure, true);
session_name($session_name = Config::get('session.name') ?: session_name()); session_name($session_name = Config::get('session.name') ?: session_name());
// Check if the session cookie already exists. // Check if the session cookie already exists.
@ -114,11 +114,7 @@ class Session
// Mark the session as started. // Mark the session as started.
self::$_started = true; self::$_started = true;
$must_create = $must_refresh = false;
// Fetch session keys.
list($key1, $key2, self::$_autologin_key) = self::_getKeys();
$must_create = $must_refresh = $must_resend_keys = false;
$check_keys = config('session.use_keys');
// Check whether the visitor uses Android webview. // Check whether the visitor uses Android webview.
if (!isset($_SESSION['is_webview'])) if (!isset($_SESSION['is_webview']))
@ -126,66 +122,14 @@ class Session
$_SESSION['is_webview'] = self::_isBuggyUserAgent(); $_SESSION['is_webview'] = self::_isBuggyUserAgent();
} }
// Validate the HTTP key. // Check if the session has been initialized for Rhymix.
if (isset($_SESSION['RHYMIX']) && $_SESSION['RHYMIX']) if (!isset($_SESSION['RHYMIX']))
{
if (!isset($_SESSION['RHYMIX']['keys'][$alt_domain]) && config('use_sso'))
{
$must_refresh = true;
}
elseif ($_SESSION['RHYMIX']['keys'][$alt_domain]['key1'] === $key1 && $key1 !== null)
{
// OK
}
elseif ($_SESSION['RHYMIX']['keys'][$alt_domain]['key1_prev'] === $key1 && $key1 !== null)
{
$must_resend_keys = true;
}
elseif ($check_keys && !$_SESSION['is_webview'])
{
// Hacked session! Destroy everything.
trigger_error('Session is invalid (missing key 1)', \E_USER_WARNING);
$_SESSION = array();
$must_create = true;
self::destroyAutologinKeys();
}
}
else
{ {
$must_create = true; $must_create = true;
} }
// Validate the SSL key. // Check if the session needs to be refreshed.
if (!$must_create && \RX_SSL) if (!$must_create && !isset($_SESSION['RHYMIX']['domains'][$alt_domain]['started']) || $_SESSION['RHYMIX']['domains'][$alt_domain]['started'] < time() - $refresh_interval)
{
if (!isset($_SESSION['RHYMIX']['keys'][$alt_domain]['key2']))
{
$must_refresh = true;
}
elseif ($_SESSION['RHYMIX']['keys'][$alt_domain]['key2'] === $key2 && $key2 !== null)
{
// OK
}
elseif ($_SESSION['RHYMIX']['keys'][$alt_domain]['key2_prev'] === $key2 && $key2 !== null)
{
$must_resend_keys = true;
}
elseif ($check_keys && !$_SESSION['is_webview'])
{
// Hacked session! Destroy everything.
trigger_error('Session is invalid (missing key 2)', \E_USER_WARNING);
$_SESSION = array();
$must_create = true;
self::destroyAutologinKeys();
}
}
// Check the refresh interval.
if (!$must_create && $_SESSION['RHYMIX']['keys'][$alt_domain]['key1_time'] < time() - $refresh_interval && $check_keys)
{
$must_refresh = true;
}
elseif (!$must_create && \RX_SSL && $_SESSION['RHYMIX']['keys'][$alt_domain]['key2_time'] < time() - $refresh_interval && $check_keys)
{ {
$must_refresh = true; $must_refresh = true;
} }
@ -228,10 +172,6 @@ class Session
{ {
return self::refresh(true); return self::refresh(true);
} }
elseif ($must_resend_keys)
{
return self::_setKeys();
}
else else
{ {
$_SESSION['is_new_session'] = false; $_SESSION['is_new_session'] = false;
@ -431,6 +371,7 @@ class Session
$_SESSION['RHYMIX']['language'] = \Context::getLangType(); $_SESSION['RHYMIX']['language'] = \Context::getLangType();
$_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser(); $_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser();
$_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum'); $_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum');
$_SESSION['RHYMIX']['domains'] = array();
$_SESSION['RHYMIX']['tokens'] = array(); $_SESSION['RHYMIX']['tokens'] = array();
$_SESSION['RHYMIX']['token'] = false; $_SESSION['RHYMIX']['token'] = false;
$_SESSION['is_webview'] = self::_isBuggyUserAgent(); $_SESSION['is_webview'] = self::_isBuggyUserAgent();
@ -450,6 +391,7 @@ class Session
} }
// Try autologin. // Try autologin.
self::$_autologin_key = self::_getAutologinKey();
if (!$member_srl && self::$_autologin_key) if (!$member_srl && self::$_autologin_key)
{ {
$member_srl = \MemberController::getInstance()->doAutologin(self::$_autologin_key); $member_srl = \MemberController::getInstance()->doAutologin(self::$_autologin_key);
@ -464,7 +406,7 @@ class Session
} }
} }
// Pass control to refresh() to generate security keys. // Pass control to refresh() to generate domain information.
return self::refresh(); return self::refresh();
} }
@ -474,47 +416,41 @@ class Session
* This method can be used to invalidate old session cookies. * This method can be used to invalidate old session cookies.
* It is called automatically when someone logs in or out. * It is called automatically when someone logs in or out.
* *
* @param bool $set_session_cookie * @param bool $refresh_cookie
* @return bool * @return bool
*/ */
public static function refresh($set_session_cookie = false) public static function refresh($refresh_cookie = false)
{ {
// Get session parameters. // Get session parameters.
$domain = self::getDomain() ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST'] ?? '')); list($lifetime, $refresh_interval, $domain, $path, $secure, $samesite) = self::_getParams();
$lifetime = $lifetime ? ($lifetime + time()) : 0;
$options = array(
'expires' => $lifetime,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => $samesite,
);
// Set the domain initialization timestamp. // Set the domain initialization timestamp.
if (!isset($_SESSION['RHYMIX']['keys'][$domain]['started'])) if (!isset($_SESSION['RHYMIX']['domains'][$domain]['started']))
{ {
$_SESSION['RHYMIX']['keys'][$domain]['started'] = time(); $_SESSION['RHYMIX']['domains'][$domain]['started'] = time();
} }
// Reset the trusted information. // Reset the trusted information.
if (!isset($_SESSION['RHYMIX']['keys'][$domain]['trusted'])) if (!isset($_SESSION['RHYMIX']['domains'][$domain]['trusted']))
{ {
$_SESSION['RHYMIX']['keys'][$domain]['trusted'] = 0; $_SESSION['RHYMIX']['domains'][$domain]['trusted'] = 0;
} }
// Create or refresh the HTTP-only key. // Refresh the main session cookie.
if (isset($_SESSION['RHYMIX']['keys'][$domain]['key1'])) if ($refresh_cookie)
{ {
$_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] = $_SESSION['RHYMIX']['keys'][$domain]['key1']; self::_setCookie(session_name(), session_id(), $options);
self::destroyCookiesFromConflictingDomains(array(session_name()));
} }
$_SESSION['RHYMIX']['keys'][$domain]['key1'] = Security::getRandom(24, 'alnum');
$_SESSION['RHYMIX']['keys'][$domain]['key1_time'] = time();
// Create or refresh the HTTPS-only key.
if (\RX_SSL)
{
if (isset($_SESSION['RHYMIX']['keys'][$domain]['key2']))
{
$_SESSION['RHYMIX']['keys'][$domain]['key2_prev'] = $_SESSION['RHYMIX']['keys'][$domain]['key2'];
}
$_SESSION['RHYMIX']['keys'][$domain]['key2'] = Security::getRandom(24, 'alnum');
$_SESSION['RHYMIX']['keys'][$domain]['key2_time'] = time();
}
// Pass control to _setKeys() to send the keys to the client.
return self::_setKeys($set_session_cookie);
} }
/** /**
@ -554,7 +490,6 @@ class Session
list($lifetime, $refresh_interval, $domain, $path, $secure, $samesite) = self::_getParams(); list($lifetime, $refresh_interval, $domain, $path, $secure, $samesite) = self::_getParams();
// Delete all cookies. // Delete all cookies.
self::_setKeys();
self::destroyAutologinKeys(); self::destroyAutologinKeys();
self::_unsetCookie(session_name(), $path, $domain); self::_unsetCookie(session_name(), $path, $domain);
self::_unsetCookie('xe_logged', $path, $domain); self::_unsetCookie('xe_logged', $path, $domain);
@ -611,7 +546,7 @@ class Session
if ($refresh) if ($refresh)
{ {
self::checkLoginStatusCookie(); self::checkLoginStatusCookie();
return self::refresh(); return self::refresh(true);
} }
else else
{ {
@ -685,7 +620,7 @@ class Session
$domain = self::getDomain() ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST'])); $domain = self::getDomain() ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST']));
// Check the 'trusted' parameter. // Check the 'trusted' parameter.
if ($_SESSION['RHYMIX']['keys'][$domain]['trusted'] > time()) if ($_SESSION['RHYMIX']['domains'][$domain]['trusted'] > time())
{ {
return true; return true;
} }
@ -887,9 +822,9 @@ class Session
$domain = self::getDomain() ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST'])); $domain = self::getDomain() ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST']));
// Update the 'trusted' parameter if the current user is logged in. // Update the 'trusted' parameter if the current user is logged in.
if (isset($_SESSION['RHYMIX']['keys'][$domain]) && $_SESSION['RHYMIX']['login']) if (isset($_SESSION['RHYMIX']['domains'][$domain]) && $_SESSION['RHYMIX']['login'])
{ {
$_SESSION['RHYMIX']['keys'][$domain]['trusted'] = time() + $duration; $_SESSION['RHYMIX']['domains'][$domain]['trusted'] = time() + $duration;
return true; return true;
} }
else else
@ -1132,86 +1067,21 @@ class Session
} }
/** /**
* Get session keys. * Get the autologin key from the rx_autologin cookie.
* *
* @return array * @return string|null
*/ */
protected static function _getKeys() protected static function _getAutologinKey()
{ {
// Initialize keys.
$key1 = $key2 = $key3 = null;
// Fetch and validate the HTTP-only key.
if (isset($_COOKIE['rx_sesskey1']) && ctype_alnum($_COOKIE['rx_sesskey1']) && strlen($_COOKIE['rx_sesskey1']) === 24)
{
$key1 = $_COOKIE['rx_sesskey1'];
}
// Fetch and validate the HTTPS-only key.
if (isset($_COOKIE['rx_sesskey2']) && ctype_alnum($_COOKIE['rx_sesskey2']) && strlen($_COOKIE['rx_sesskey2']) === 24)
{
$key2 = $_COOKIE['rx_sesskey2'];
}
// Fetch and validate the autologin key. // Fetch and validate the autologin key.
if (isset($_COOKIE['rx_autologin']) && ctype_alnum($_COOKIE['rx_autologin']) && strlen($_COOKIE['rx_autologin']) === 48) if (isset($_COOKIE['rx_autologin']) && ctype_alnum($_COOKIE['rx_autologin']) && strlen($_COOKIE['rx_autologin']) === 48)
{ {
$key3 = $_COOKIE['rx_autologin']; return $_COOKIE['rx_autologin'];
}
return array($key1, $key1 === null ? null : $key2, $key3);
}
/**
* Set session keys.
*
* @param bool $set_session_cookie
* @return bool
*/
protected static function _setKeys($set_session_cookie = false)
{
// Get session parameters.
list($lifetime, $refresh_interval, $domain, $path, $secure, $samesite) = self::_getParams();
$alt_domain = $domain ?: preg_replace('/:\\d+$/', '', strtolower($_SERVER['HTTP_HOST'] ?? ''));
$lifetime = $lifetime ? ($lifetime + time()) : 0;
$options = array(
'expires' => $lifetime,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => $samesite,
);
// Refresh the main session cookie.
if ($set_session_cookie)
{
self::_setCookie(session_name(), session_id(), $options);
}
// Set or destroy the HTTP-only key.
if (isset($_SESSION['RHYMIX']['keys'][$alt_domain]['key1']))
{
self::_setCookie('rx_sesskey1', $_SESSION['RHYMIX']['keys'][$alt_domain]['key1'], $options);
$_COOKIE['rx_sesskey1'] = $_SESSION['RHYMIX']['keys'][$alt_domain]['key1'];
} }
else else
{ {
self::_unsetCookie('rx_sesskey1', $path, $domain); return null;
unset($_COOKIE['rx_sesskey1']);
} }
// Set the HTTPS-only key.
if (\RX_SSL && isset($_SESSION['RHYMIX']['keys'][$alt_domain]['key2']))
{
$options['secure'] = true;
self::_setCookie('rx_sesskey2', $_SESSION['RHYMIX']['keys'][$alt_domain]['key2'], $options);
$_COOKIE['rx_sesskey2'] = $_SESSION['RHYMIX']['keys'][$alt_domain]['key2'];
}
// Delete conflicting domain cookies.
self::destroyCookiesFromConflictingDomains(array(session_name(), 'rx_autologin', 'rx_login_status', 'rx_sesskey1', 'rx_sesskey2'));
return true;
} }
/** /**
@ -1227,7 +1097,7 @@ class Session
$name = strval($name); $name = strval($name);
$value = strval($value); $value = strval($value);
if (version_compare(PHP_VERSION, '7.3', '>=')) if (PHP_VERSION_ID >= 70300)
{ {
$result = setcookie($name, $value, $options); $result = setcookie($name, $value, $options);
} }