Reputation: 1127
I've been trying to solve this problem for many weeks now.
I've made a class to secure PHP sessions, and it works fine unless someone is trying to perform registration (its the problem #2) and if some of the functionality disabled (which is causing #2 to happen), the rest of the website work just fine.
So here are the problems:
I know where the problem might be, but i dont know how to solve it.
I have a private static function, which is called straight after session_start() is called
private static function GenerateSessionData()
{
$_SESSION['loggedin'] = '';
$_SESSION['username'] = '';
$_SESSION['remember_me'] = '';
$_SESSION['preferredlanguage'] = '';
$_SESSION['generated_captcha'] = '';
}
This is done to pre-define variables to be used by session (and i'm 90% sure that this is why session goes blank after).
The thing im not sure about, is WHY.
Here is the full session class:
<?php
Class Session
{
public static $DBConnection;
private static $SessionCreated = false;
public function __construct($Database)
{
session_set_save_handler(array($this, 'Open'), array($this, 'Close'), array($this, 'Read'), array($this, 'Write'), array($this, 'Destroy'), array($this, 'GarbageCollector'));
register_shutdown_function('session_write_close');
Session::$DBConnection = $Database::$Connection;
}
private static function GenerateSessionData()
{
$_SESSION['loggedin'] = '';
$_SESSION['username'] = '';
$_SESSION['remember_me'] = '';
$_SESSION['preferredlanguage'] = '';
$_SESSION['generated_captcha'] = '';
}
public static function UpdateSession($Data)
{
if(!isset($_SESSION['loggedin']))
Session::GenerateSessionData();
foreach($Data as $key=>$value)
$_SESSION[$key] = $value;
}
public static function GenerateCSRFToken()
{
$InitialString = "abcdefghijklmnopqrstuvwxyz1234567890";
$PartOne = substr(str_shuffle($InitialString),0,8);
$PartTwo = substr(str_shuffle($InitialString),0,4);
$PartThree = substr(str_shuffle($InitialString),0,4);
$PartFour = substr(str_shuffle($InitialString),0,4);
$PartFive = substr(str_shuffle($InitialString),0,12);
$FinalCode = $PartOne.'-'.$PartTwo.'-'.$PartThree.'-'.$PartFour.'-'.$PartFive;
$_SESSION['generated_csrf'] = $FinalCode;
return $FinalCode;
}
public static function ValidateCSRFToken($Token)
{
if(isset($Token) && $Token == $_SESSION['generated_csrf'])
{
unset($_SESSION['generated_csrf']);
return true;
}
else
return false;
}
public static function UnsetKeys($Keys)
{
foreach($Keys as $Key)
unset($_SESSION[$Key]);
}
public static function Start($SessionName, $Secure)
{
$HTTPOnly = true;
$Session_Hash = 'sha512';
if(in_array($Session_Hash, hash_algos()))
ini_set('session.hash_function', $Session_Hash);
ini_set('session.hash_bits_per_character', 6);
ini_set('session.use_only_cookies', 1);
$CookieParameters = session_get_cookie_params();
session_set_cookie_params($CookieParameters["lifetime"], $CookieParameters["path"], $CookieParameters["domain"], $Secure, $HTTPOnly);
session_name($SessionName);
session_start();
session_regenerate_id(true);
if(!Session::$SessionCreated)
if(!isset($_SESSION['loggedin']))
Session::GenerateSessionData();
Session::$SessionCreated = true;
}
static function Open()
{
if(is_null(Session::$DBConnection))
{
die("Unable to establish connection with database for Secure Session!");
return false;
}
else
return true;
}
static function Close()
{
Session::$DBConnection = null;
return true;
}
static function Read($SessionID)
{
$Statement = Session::$DBConnection->prepare("SELECT data FROM sessions WHERE id = :sessionid LIMIT 1");
$Statement->bindParam(':sessionid', $SessionID);
$Statement->execute();
$Result = $Statement->fetch(PDO::FETCH_ASSOC);
$Key = Session::GetKey($SessionID);
$Data = Session::Decrypt($Result['data'], $Key);
return $Data;
}
static function Write($SessionID, $SessionData)
{
$Key = Session::GetKey($SessionID);
$Data = Session::Encrypt($SessionData, $Key);
$TimeNow = time();
$Statement = Session::$DBConnection->prepare('REPLACE INTO sessions (id, set_time, data, session_key) VALUES (:sessionid, :creation_time, :session_data, :session_key)');
$Statement->bindParam(':sessionid', $SessionID);
$Statement->bindParam(':creation_time', $TimeNow);
$Statement->bindParam(':session_data', $Data);
$Statement->bindParam(':session_key', $Key);
$Statement->execute();
return true;
}
static function Destroy($SessionID)
{
$Statement = Session::$DBConnection->prepare('DELETE FROM sessions WHERE id = :sessionid');
$Statement->bindParam(':sessionid', $SessionID);
$Statement->execute();
Session::$SessionCreated = false;
return true;
}
private static function GarbageCollector($Max)
{
$Statement = Session::$DBConnection->prepare('DELETE FROM sessions WHERE set_time < :maxtime');
$OldSessions = time()-$Max;
$Statement->bindParam(':maxtime', $OldSessions);
$Statement->execute();
return true;
}
private static function GetKey($SessionID)
{
$Statement = Session::$DBConnection->prepare('SELECT session_key FROM sessions WHERE id = :sessionid LIMIT 1');
$Statement->bindParam(':sessionid', $SessionID);
$Statement->execute();
$Result = $Statement->fetch(PDO::FETCH_ASSOC);
if($Result['session_key'] != '')
return $Result['session_key'];
else
return hash('sha512', uniqid(mt_rand(1, mt_getrandmax()), true));
}
private static function Encrypt($SessionData, $SessionKey)
{
$Salt = "06wirrdzHDvc*t*nJn9VWIfET+|co*pm~CbtT5P*S2IPD-VmEfd+CX2wrvZ";
$SessionKey = substr(hash('sha256', $Salt.$SessionKey.$Salt), 0, 32);
$Get_IV_Size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$IV = mcrypt_create_iv($Get_IV_Size, MCRYPT_RAND);
$Encrypted = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $SessionKey, $SessionData, MCRYPT_MODE_ECB, $IV));
return $Encrypted;
}
private static function Decrypt($SessionData, $SessionKey)
{
$Salt = "06wirrdzHDvc*t*nJn9VWIfET+|co*pm~CbtT5P*S2IPD-VmEfd+CX2wrvZ";
$SessionKey = substr(hash('sha256', $Salt.$SessionKey.$Salt), 0, 32);
$Get_IV_Size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$IV = mcrypt_create_iv($Get_IV_Size, MCRYPT_RAND);
$Decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $SessionKey, base64_decode($SessionData), MCRYPT_MODE_ECB, $IV);
return $Decrypted;
}
}
?>
And i cant exclude that private static function (the first one mentioned) since then i wont be able to set the variables.
And you might say: 'but there is a UpdateSession method'....
Yeah.... kinda.... but the thing is, due to i guess lack of my knowledge, i messed up script somewhere, and the logic goes wrong.
Here are the links (to maybe simplify understanding):
Sessions.FreedomCore.php - Sessions Class
String.FreedomCore.php - Captcha Generation (pointed to exact line)
pager.php - Account Creation Process (works only with session_regenerate_id)
pager.php - Captcha display Process (works always in some cases)
pager.php - Perform Login Case (No issues in any case for some reason)
If you like super interested in how this actually works (i mean how it deauthorize users after 4 refreshes)
Please head over here
Username: test
Password: 123456
So the question is: How to modify my class, to save session data with session_regenerate_id(true) with usage of current methods and to prevent it to be flushed after session_regenerate_id is called.
These links are pointing directly to problematic areas of the script.
Any help is really appreciated.
Thank you very much for any help!
Upvotes: 1
Views: 1368
Reputation: 2024
You are experiencing what I call Cookie Race Condition.
As your are using session_regenerate_id(true)
PHP creates a new session id (containing the data of the old session) and deletes the old session from your database for each request.
Now your website contains contains many elements which need to be loaded, e.g. /pager.php
or /data/menu.json
. And each time the browser gets assigned a new session id. Normally not a problem, but modern browsers do requests in parallel:
pager.php
is requested with session_id = a
data/menu.json
is requested with session_id = a
pager.php
drops sessions_id = a
and returns session_id = b
to my browser.data/menu.json
cannot find session_id = a
in the database and assumes I'm a new visitor and gives me the session_id = c
Now it depends which request is received and parsed by the browser in which order.
Case A data/menu.json
is parsed first: the browser stores session_id = c
. Then the response of pager.php
is parsed and the browser overrides session_id with b. For any next request it will use session_id = b
.
Case B pager.php
is parsed first and then data/menu.json
. The browser stores now session_id = c
and you are logged out.
This explains why it's sometimes working (e.g., 4 or 6 refreshes) and sometimes not.
Conclusion: don't use session_regenerate_id();
without a very good reason!
Please raise a new question why the captcha creation mechanism does not work on registration page but on login page.
Some notes on your encryption.
Upvotes: 3