This can only happen when you share a session_id.
Under normal, proper site operations (^), that can only happen when you are issueing/using the same session_id while that session_id is already given.
Chances of that are very very small but not zero.
The change is increased when:
1) you have a huge backlog of used session records in your database (which is common in default OC) or file system. Remember, bots get a new session id on every request so that db table can fill pretty fast if not cleaned properly.
2) hackers who send requests with random/edited session cookies in the hope one is active, very rare, with virtually no chance but very difficult to counter (*).
3) Users who never close their browser and thus retain their session_id indefinitely which now may have been issued to someone else or some "remember me" implementation where the session cookie is no longer a session cookie but has a defined lifetime (*).
(*) many sites (incl OC) simply use the session_id presented in the cookie even if that session_id was not issued by the server nor whether that session is actually active on the server. As long as it is a valid format, that session_id is used.
(^) excludes unstable/experimental one-page checkout implementations
Item 1. you can counter by:
a) making sure your session garbage collection works properly which is the preferred method with the least performance impact.
b) checking whether a new session_id is already active before issueing/using it which adds extra effort, be it a minute one.
a)
For those sites we use the database for session storage we added this function to the file
system/library/session/db.php
Code: Select all
public function __destruct() {
if (ini_get('session.gc_divisor')) $gc_divisor = ini_get('session.gc_divisor');
else $gc_divisor = 1;
if (ini_get('session.gc_probability')) $gc_probability = ini_get('session.gc_probability');
else $gc_probability = 1;
if ((rand() % $gc_divisor) < $gc_probability) {
$expire_dt = date('Y-m-d H:i:s', time() - ini_get('session.gc_maxlifetime'));
$this->db->query("DELETE FROM `" . DB_PREFIX . "session` WHERE expire < '".$expire_dt."'");
}
}
That should take better care of the session garbage collection, the default gc() function is crap as it compares a timestamp with a date-time value apart from the fact that it is never called. The file storage class already has a __destruct() function and works fine.
b)
we changed the start function in
system/library/session.php to:
Code: Select all
public function start($session_id = false, $no_clash = true) {
if ($session_id) {
// determine if session id is valid format
if (!preg_match('/^[a-zA-Z0-9,\-]{22,52}$/', $session_id)) {
error_log('Error: invalid session ID given:'.$session_id.' by '.$_SERVER['REMOTE_ADDR'].', issueing a new one...');
$session_id = false;
}
}
if (!$session_id) {
// issue new session id
$session_id_active = true;
while ($session_id_active) {
// generate a new session id
if (function_exists('random_bytes')) $session_id = substr(bin2hex(random_bytes(32)), 0, 32);
else $session_id = substr(bin2hex(openssl_random_pseudo_bytes(32)), 0, 32);
// exit loop if no_clash is false or session id not active
if (!$no_clash) $session_id_active = false;
elseif (!$this->adaptor->session_active($session_id)) $session_id_active = false;
}
}
$this->session_id = $session_id;
$this->data = $this->adaptor->read($session_id);
return $session_id;
}
We added to
system/library/session/db.php
Code: Select all
public function session_active ($session_id) {
// determine if session_id is present in the session table
$this->db->query("SELECT session_id FROM " . DB_PREFIX . "session WHERE session_id = '" . $this->db->escape($session_id) . "'");
if (!$query->num_rows) return false;
return true;
}
We added to
system/library/session/file.php
Code: Select all
public function session_active ($session_id) {
// determine if session_id is present in the file system
$file = DIR_SESSION . 'sess_' . basename($session_id);
clearstatcache(true, $file);
if (!file_exists($file)) return false;
return true;
}
These function will check whether a newly generated session_id is already active server-side and generate a new one if it is.
Both for db and file stored sessions. You can toggle that check with the $no_clash parameter.
In addition, as stated (*), OC will use any session_id given to it via the cookie as long as it has the right format, which is not good.
We check whether the given session_id actually is active server-side or that given session_id is ignored.
catalog/controller/startup/session.php
change:
Code: Select all
if (isset($_COOKIE[$this->config->get('session_name')])) {
$session_id = $_COOKIE[$this->config->get('session_name')];
} else {
$session_id = '';
}
$this->session->start($session_id);
to:
Code: Select all
$session_id = '';
if (!empty($_COOKIE[$this->config->get('session_name')])) {
if (SESSION_VERIFY) {
if ($this->config->get('session_engine') == 'db') {
$query = $this->db->query("SELECT session_id FROM " . DB_PREFIX . "session WHERE session_id = '" . $this->db->escape($_COOKIE[$this->config->get('session_name')]) . "'");
if ($query->num_rows) $session_id = $_COOKIE[$this->config->get('session_name')];
} else {
$session_file = DIR_SESSION.'sess_'.$_COOKIE[$this->config->get('session_name')];
if (is_file($session_file)) $session_id = $_COOKIE[$this->config->get('session_name')];
}
} else {
$session_id = $_COOKIE[$this->config->get('session_name')];
}
}
$this->session->start($session_id,SESSION_NOCLASH);
system/framework.php
change:
Code: Select all
if (isset($_COOKIE[$config->get('session_name')])) {
$session_id = $_COOKIE[$config->get('session_name')];
} else {
$session_id = '';
}
$this->session->start($session_id);
to:
Code: Select all
$session_id = false;
if (isset($_COOKIE[$config->get('session_name')])) {
if (SESSION_VERIFY) {
if ($config->get('session_engine') == 'db') {
$query = $db->query("SELECT session_id FROM " . DB_PREFIX . "session WHERE session_id = '" . $db->escape($_COOKIE[$config->get('session_name')]) . "'");
if ($query->num_rows) $session_id = $_COOKIE[$config->get('session_name')];
} else {
$session_file = DIR_SESSION.'sess_'.$_COOKIE[$config->get('session_name')];
if (is_file($session_file)) $session_id = $_COOKIE[$config->get('session_name')];
}
} else {
$session_id = $_COOKIE[$config->get('session_name')];
}
}
$this->session->start($session_id,SESSION_NOCLASH);
change:
Code: Select all
// Database
if ($config->get('db_autostart')) {
$registry->set('db', new DB($config->get('db_engine'), $config->get('db_hostname'), $config->get('db_username'), $config->get('db_password'), $config->get('db_database'), $config->get('db_port')));
}
to:
Code: Select all
// Database
if ($config->get('db_autostart')) {
$db = new DB($config->get('db_engine'), $config->get('db_hostname'), $config->get('db_username'), $config->get('db_password'), $config->get('db_database'), $config->get('db_port'));
$registry->set('db', $db);
}
to be able to access the database from here.
(Turning verify on in framework and session controller makes the format check redundant as an invalid formatted session id would never be found in the database or file system)
define the switches in config
config.php
Code: Select all
define('SESSION_VERIFY', true); // verify if the given session id exists server side
define('SESSION_NOCLASH', false); // prevent issueing new session id which already exists
As said, under proper operations, the chance of clashing sessions is very small but not zero and when it does happen, the impact/embarassment is rather large.
So make your own choice on whether the yield is worth the extra effort.