包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
247 lines
7.3 KiB
PHP
247 lines
7.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS\Session;
|
|
|
|
use MRBS\SessionHandler\SessionHandlerDb;
|
|
use MRBS\SessionHandler\SessionHandlerDbException;
|
|
use MRBS\User;
|
|
use SessionHandler;
|
|
use function MRBS\db;
|
|
use function MRBS\db_schema_version;
|
|
use function MRBS\get_cookie_path;
|
|
use function MRBS\is_https;
|
|
|
|
abstract class Session
|
|
{
|
|
protected const SAMESITE_NONE = 'None';
|
|
protected const SAMESITE_LAX = 'Lax';
|
|
protected const SAMESITE_STRICT = 'Strict';
|
|
|
|
protected $lifetime;
|
|
protected $samesite = null;
|
|
|
|
|
|
public function __construct()
|
|
{
|
|
global $auth, $cookie_samesite_lax;
|
|
|
|
// Child classes can set $this->samesite
|
|
if (!isset($this->samesite))
|
|
{
|
|
$this->samesite = ($cookie_samesite_lax) ? self::SAMESITE_LAX : self::SAMESITE_STRICT;
|
|
}
|
|
|
|
// Set the session lifetime
|
|
if (!isset($this->lifetime))
|
|
{
|
|
$this->lifetime = $auth['session_php']['session_expire_time'] ?? 0;
|
|
}
|
|
|
|
// Start up sessions
|
|
$this->init($this->lifetime);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the session handler to use.
|
|
*
|
|
* If the database session handler is not available, use the ordinary PHP session handler.
|
|
*
|
|
* @return SessionHandlerDb|SessionHandler
|
|
*/
|
|
protected function getSessionHandler()
|
|
{
|
|
// The sessions table was only created in Upgrade 56. We test for the schema version rather than the existence of
|
|
// the table, because the table is renamed in later upgrades.
|
|
if (db_schema_version(db()) < 56)
|
|
{
|
|
return new SessionHandler();
|
|
}
|
|
|
|
// The DB session handler uses locks, and because we use locks elsewhere, this means we need support for multiple
|
|
// locks. We need to test now, rather than catching an exception later, because resetting the session handler
|
|
// will reset the session id causing us to lose session data.
|
|
if (!db()->supportsMultipleLocks())
|
|
{
|
|
$message = "The database server does not support multiple locks, so the database session handler " .
|
|
"cannot be used. Using ordinary PHP sessions instead.";
|
|
trigger_error($message);
|
|
return new SessionHandler();
|
|
}
|
|
|
|
// Otherwise use the DB session handler.
|
|
return new SessionHandlerDb();
|
|
}
|
|
|
|
|
|
// Normally there's no need to call init() from outside the Session classes.
|
|
// It only needs to be called to restart sessions, after, for example, a user
|
|
// has been logged off, and you need to use session variables.
|
|
public function init(int $lifetime) : void
|
|
{
|
|
global $auth;
|
|
|
|
if (session_status() === PHP_SESSION_ACTIVE)
|
|
{
|
|
// We've already started sessions
|
|
return;
|
|
}
|
|
|
|
// Session settings, for security
|
|
// ini_set() only accepts string values prior to PHP 8.1.0
|
|
ini_set('session.cookie_httponly', '1');
|
|
if (version_compare(PHP_VERSION, '7.3', '>='))
|
|
{
|
|
// Only introduced in PHP Version 7.3
|
|
ini_set('session.cookie_samesite', $this->samesite);
|
|
}
|
|
ini_set('session.cookie_secure', (is_https()) ? '1' : '0');
|
|
|
|
// More settings, as a defence against session fixation.
|
|
ini_set('session.use_only_cookies', '1');
|
|
ini_set('session.use_strict_mode', '1');
|
|
ini_set('session.use_trans_sid', '0');
|
|
|
|
$cookie_path = get_cookie_path();
|
|
|
|
// We don't want the session garbage collector to delete the session before it has expired
|
|
if ($lifetime !== 0)
|
|
{
|
|
assert(version_compare(MRBS_MIN_PHP_VERSION, '8.1') < 0, 'The strval() in the line below is no longer required.');
|
|
ini_set('session.gc_maxlifetime', strval(max(ini_get('session.gc_maxlifetime'), $lifetime)));
|
|
}
|
|
|
|
if (isset($auth['session_php']['session_name']))
|
|
{
|
|
// call before session_set_cookie_params() - see PHP manual
|
|
session_name($auth['session_php']['session_name']);
|
|
}
|
|
session_set_cookie_params($lifetime, $cookie_path);
|
|
|
|
// Set the session handler and start up sessions
|
|
try
|
|
{
|
|
session_set_save_handler($this->getSessionHandler(), true);
|
|
if (false === session_start())
|
|
{
|
|
throw new \Exception("session_start() failed");
|
|
}
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$message = "Could not start sessions ('" . $e->getMessage() . "').";
|
|
$message .= " Trying ordinary PHP sessions.";
|
|
trigger_error($message, E_USER_WARNING);
|
|
session_set_save_handler(new SessionHandler(), true);
|
|
if (false === session_start())
|
|
{
|
|
throw new \Exception("MRBS: could not start sessions");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
protected function destroy() : void
|
|
{
|
|
// Delete the session data encryption key cookie. If we don't do this then, when
|
|
// a new session is created, unless the expiry is set to 0 (ie on browser close),
|
|
// it will have a longer lifetime than the key cookie, which when it was created
|
|
// was given the same lifetime as the session cookie. Once the key cookie expires,
|
|
// the session handler will create a new cookie with a new key. So when the session
|
|
// handler comes to decrypt the session data it will be doing so with the new key,
|
|
// and not the key used to encrypt it. This will result in the Crypto library
|
|
// throwing a WrongKeyOrModifiedCiphertextException with the message "Integrity
|
|
// check failed".
|
|
//
|
|
// This needs to be done before the session is destroyed, otherwise the
|
|
// deleteKeyCookie method won't be able to get the session name (which it needs
|
|
// in order to delete the key cookie).
|
|
SessionHandlerDb::deleteKeyCookie();
|
|
|
|
// Unset the session variables
|
|
// Note that session_unset() only works if a session is active.
|
|
$_SESSION = [];
|
|
// Check whether a session is active before destroying it in order to avoid a
|
|
// "Trying to destroy uninitialized session" warning.
|
|
if (session_status() === PHP_SESSION_ACTIVE)
|
|
{
|
|
session_destroy();
|
|
}
|
|
|
|
// Problems have been reported on Windows IIS with session data not being
|
|
// written out without a call to session_write_close(). [Is this necessary
|
|
// after session_destroy() ??]
|
|
session_write_close();
|
|
}
|
|
|
|
|
|
protected function regenerate() : void
|
|
{
|
|
// Regenerate the session id
|
|
session_regenerate_id(true);
|
|
|
|
// Change the lifetime of the key cookie to match the new expiry - see the
|
|
// comment in destroy().
|
|
SessionHandlerDb::regenerateKeyCookie();
|
|
}
|
|
|
|
|
|
public function get(string $name)
|
|
{
|
|
return $_SESSION[$name] ?? null;
|
|
}
|
|
|
|
|
|
public function isset(string $name) : bool
|
|
{
|
|
return isset($_SESSION[$name]);
|
|
}
|
|
|
|
public function set(string $name, $value) : void
|
|
{
|
|
$_SESSION[$name] = $value;
|
|
}
|
|
|
|
|
|
public function unset(string $name) : void
|
|
{
|
|
unset($_SESSION[$name]);
|
|
}
|
|
|
|
|
|
// Returns the currently logged-in user
|
|
// This method provides the fallback user for un-logged in users.
|
|
// Subclasses are expected to override this method, calling it as the parent
|
|
// if they cannot find a current user.
|
|
public function getCurrentUser() : ?User
|
|
{
|
|
global $auth;
|
|
|
|
if (empty($auth['allow_anonymous_booking']))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Use an empty string for anonymous bookings
|
|
return new User('');
|
|
}
|
|
|
|
|
|
// Allows this to be extended with strategies for getting the referer when
|
|
// HTTP_REFERER is going to be unreliable, eg when the Referrer-Policy is
|
|
// set to strict-origin.
|
|
public function getReferrer() : ?string
|
|
{
|
|
global $server;
|
|
|
|
return $server['HTTP_REFERER'] ?? null;
|
|
}
|
|
|
|
|
|
// Updates the current and previous pages
|
|
public function updatePage(string $url) : void
|
|
{
|
|
}
|
|
|
|
}
|