darki73
darki73

Reputation: 1127

PHP Sessions and session_regenerate_id

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:

  1. session_regenerate_id - commented out
    From here, everything works just fine, except for captcha creation mechanism (only on registration page), for login page it works just fine
  2. session_regenerate_id(true) - uncommented
    In here, registration works just fine, no problems, no captcha issues, but after few refreshes of the page, session just gone, so user need to log in for 6 more refreshes and $_SESSION again sets to null

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

Answers (1)

maxwilms
maxwilms

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:

  1. pager.php is requested with session_id = a
  2. data/menu.json is requested with session_id = a
  3. pager.php drops sessions_id = a and returns session_id = b to my browser.
  4. 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.

  1. Do NOT use AES with ECB Mode. This weakens the encryption.
  2. You store the encryption key next to your data. Your encryption just blew up.

Upvotes: 3

Related Questions