CVE-2022-31888: Session Fixation (CWE-384)
value: Session Fixation vulnerability in in function login in class.auth.php in osTicket through 1.16.2.
CWE description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
CVSS v3 base score: 8.8
Published: 2023-04-05T22:15Z
Repository: https://github.com/osTicket/osTicket
Language: PHP
Files changed: include/class.auth.php, include/class.usersession.php, open.php
Commit date: 2022-05-19 15:21:48 +0000
Commit message: Security: Session Fixation

This commit addresses possible session fixation on both agent and end user
login. Upon login users are now issued new session with the old one
invalidated.

Diff: diff --git a/include/class.auth.php b/include/class.auth.php
index e1e864a946..fc4c53f421 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -536,10 +536,12 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
         // Ensure staff is allowed for realz to be authenticated via the backend.
         if (!static::isBackendAllowed($staff, $bk)
             || !($authkey=$bk->getAuthKey($staff)))
             return false;
 
+        // Regenerate session id and refresh (avoid session fixation)
+        $staff->regenerateSession();
         //Log debug info.
         $ost->logDebug(_S('Agent Login'),
             sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
                 $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
 
@@ -561,13 +563,14 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
         $authsession = &$_SESSION['_auth']['staff'];
         $authsession = array(); //clear.
         $authsession['id'] = $staff->getId();
         $authsession['key'] =  $authkey;
         $authsession['2fa'] =  $auth2fa;
-
+        // Set session token
+        $staff->setSessionToken();
+        // Set Auth Key
         $staff->setAuthKey($authkey);
-        $staff->refreshSession(true); //set the hash.
         Signal::send('auth.login.succeeded', $staff);
 
         if ($bk->supportsInteractiveAuthentication())
             $staff->cancelResetTokens();
 
@@ -753,19 +756,19 @@ abstract class UserAuthenticationBackend  extends AuthenticationBackend {
                 throw new AccessDenied(__('Account confirmation required'));
             elseif ($acct->isLocked())
                 throw new AccessDenied(__('Account is administratively locked'));
         }
 
+        // Regenerate session id and refresh (avoid session fixation)
+        $user->regenerateSession();
         // Tag the user and associated ticket in the SESSION
         $this->setAuthKey($user, $bk, $authkey);
-
+        // Set Session Token
+        $user->setSessionToken();
         //The backend used decides the format of the auth key.
         // XXX: encrypt to hide the bk??
         $user->setAuthKey($authkey);
-
-        $user->refreshSession(true); //set the hash.
-
         //Log login info...
         $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]'
                 /* Tokens are <username>, <id>, and <ip> */),
                 $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']);
         $ost->logDebug(_S('User login'), $msg);
diff --git a/include/class.usersession.php b/include/class.usersession.php
index 9184087637..99df20020f 100644
--- a/include/class.usersession.php
+++ b/include/class.usersession.php
@@ -15,218 +15,245 @@
 **********************************************************************/
 
 include_once(INCLUDE_DIR.'class.client.php');
 include_once(INCLUDE_DIR.'class.staff.php');
 
-
 class UserSession {
    var $session_id = '';
-   var $userID='';
+   var $userID = 0;
    var $browser = '';
    var $ip = '';
-   var $validated=FALSE;
-
-   function __construct($userid){
+   var $validated = false;
 
-      $this->browser=(!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
-      $this->ip=(!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
-      $this->session_id=session_id();
-      $this->userID=$userid;
+   function __construct($userid) {
+      $this->browser = (!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
+      $this->ip = (!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
+      $this->session_id = session_id();
+      $this->userID = $userid;
    }
 
-   function isStaff(){
-       return FALSE;
+   function isStaff() {
+       return false;
    }
 
    function isClient() {
-       return FALSE;
+       return false;
    }
 
-
-   function getSessionId(){
+   function getSessionId() {
        return $this->session_id;
    }
 
-   function getIP(){
+   function getIP() {
         return  $this->ip;
    }
 
-   function getBrowser(){
+   function getBrowser() {
        return $this->browser;
    }
-   function refreshSession(){
-       //nothing to do...clients need to worry about it.
-   }
 
    function sessionToken(){
-
       $time  = time();
       $hash  = md5($time.SESSION_SECRET.$this->userID);
       $token = "$hash:$time:".MD5($this->ip);
-
-      return($token);
+      return $token;
    }
 
    function getLastUpdate($htoken) {
        if (!$htoken)
            return 0;
 
-       @list($hash,$expire,$ip)=explode(":",$htoken);
+       @list($hash, $expire, $ip) = explode(":", $htoken);
        return $expire;
    }
 
    function isvalidSession($htoken,$maxidletime=0,$checkip=false){
-        global $cfg;
+        global $cfgi;
 
-        $token = rawurldecode($htoken);
+        // Compare session ids
+        if (strcmp($this->getSessionId(), session_id()))
+            return false;
 
-        #check if we got what we expected....
-        if($token && !strstr($token,":"))
-            return FALSE;
+        // Is the session invalidated?
+        if (isset($_SESSION['KAPUT']) &&  $_SESSION['KAPUT'] < time())
+            return (session_destroy() && false);
 
-        #get the goodies
-        list($hash,$expire,$ip)=explode(":",$token);
+        $token = rawurldecode($htoken);
+        // Check if we got what we expected....
+        if ($token && !strstr($token,":"))
+            return false;
 
-        #Make sure the session hash is valid
-        if((md5($expire . SESSION_SECRET . $this->userID)!=$hash)){
-            return FALSE;
-        }
-        #is it expired??
+        // Get the goodies
+        list($hash, $expire, $ip) = explode(":",$token);
 
+        // Make sure the session hash is valid
+        if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
+            return false;
+
+        // is it expired??
+        if ($maxidletime && ((time()-$expire)>$maxidletime))
+            return false;
 
-        if($maxidletime && ((time()-$expire)>$maxidletime)){
-            return FALSE;
-        }
         #Make sure IP is still same ( proxy access??????)
-        if($checkip && strcmp($ip, MD5($this->ip)))
-            return FALSE;
+        if ($checkip && strcmp($ip, MD5($this->ip)))
+            return false;
+
+        $this->validated = true;
 
-        $this->validated=TRUE;
+        return true;
+   }
+
+   function regenerateSession($destroy=false) {
+       // Delayed kaput time for current session
+       $_SESSION['KAPUT'] = time() + 60;
+       // Save the session id as old
+       $old = session_id();
+       // Regenerate the session without destroying data
+       session_regenerate_id(false);
+       // Get new session id and close
+       $new = session_id();
+       session_write_close();
+       // Start new session
+       session_id($new);
+       session_start();
+       $this->session_id  = $new;
+       // Make sure new session is not set to KAPUT
+       unset($_SESSION['KAPUT']);
+       // Destroy ?
+       if ($destroy) {
+           // Destrory old session
+           $this->destroySession($old);
+           // Restore new session
+           session_id($new);
+           session_start();
+       }
+       return true;
+   }
 
-        return TRUE;
+   function destroySession($id) {
+       // Close current session
+       session_write_close();
+       // Start target session
+       session_id($id);
+       session_start();
+       // Destroy session
+       session_destroy();
+       session_write_close();
+       return true;
    }
 
    function isValid() {
-        return FALSE;
+        return  ($this->validated);
    }
 
 }
 
-class ClientSession extends EndUser {
 
+trait UserSessionTrait {
+    // Session Object
     var $session;
+    // Session Token
     var $token;
-
-    function __construct($user) {
-        parent::__construct($user);
-        $this->token = &$_SESSION[':token']['client'];
-        // XXX: Change the key to user-id
-        $this->session= new UserSession($user->getId());
-    }
-
-    function getSessionUser() {
-        return $this->user;
-    }
-
-    function isValid(){
-        global $_SESSION,$cfg;
-
-        if(!$this->getId() || $this->session->getSessionId()!=session_id())
-            return false;
-
-        return $this->session->isvalidSession($this->token,$cfg->getClientTimeout(),false)?true:false;
-    }
-
-    function refreshSession($force=false){
-        global $cfg;
-
+    // Maximum idle time before session is considered invalid
+    var $maxidletime = 0;
+    // Indicates if session is bound to the IP address
+    var $checkip = false;
+    // User class
+    var $class = '';
+
+    function refreshSession($force=false) {
         $time = $this->session->getLastUpdate($this->token);
         // Deadband session token updates to once / 30-seconds
         if (!$force && time() - $time < 30)
             return;
 
         $this->token = $this->getSessionToken();
-        //TODO: separate expire time from hash??
+        osTicketSession::renewCookie($time, $this->maxidletime);
+    }
 
-        osTicketSession::renewCookie($time, $cfg->getClientSessionTimeout());
+    function regenerateSession($destroy=false) {
+        $this->session->regenerateSession($destroy);
+        // Set cookie for the new session id.
+        $this->refreshSession(true);
     }
 
     function getSession() {
         return $this->session;
     }
 
     function getSessionToken() {
         return $this->session->sessionToken();
     }
 
-    function getIP(){
+    function setSessionToken($token=null) {
+        // Assign memory to token variable
+        $this->token = &$_SESSION[':token'][$this->class];
+        // Set token
+        $this->token = $token ?: $this->getSessionToken();
+    }
+
+    function getIP() {
         return $this->session->getIP();
     }
+
+    function isValidSession() {
+        return ($this->getId()
+                && $this->session->isvalidSession($this->token,
+                    $this->maxidletime, $this->checkip));
+    }
+
+    abstract function isValid();
 }
 
+class ClientSession extends EndUser {
+    use UserSessionTrait;
 
-class StaffSession extends Staff {
+    function __construct($user) {
+        global $cfg;
+        parent::__construct($user);
+        $this->class ='client';
+        // XXX: Change the key to user-id
+        $this->session = new UserSession($user->getId());
+        $this->setSessionToken();
+        $this->maxidletime = $cfg->getClientTimeout();
+    }
 
-    var $session;
-    var $token;
+    function getSessionUser() {
+        return $this->user;
+    }
+
+    function isValid() {
+        return $this->isValidSession();
+    }
+}
+
+class StaffSession extends Staff {
+    use UserSessionTrait;
 
     static function lookup($var) {
-        if ($staff = parent::lookup($var)) {
-            $staff->token = &$_SESSION[':token']['staff'];
-            $staff->session= new UserSession($staff->getId());
+        global $cfg;
+        if (($staff = parent::lookup($var))) {
+            $staff->class = 'staff';
+            $staff->session = new UserSession($staff->getId());
+            $staff->setSessionToken();
+            $staff->maxidletime = $cfg->getStaffTimeout();
+            $staff->checkip = $cfg->enableStaffIPBinding();
         }
         return $staff;
     }
 
     function clear2FA() {
+        unset($_SESSION['_auth']['staff']['2fa']);
         $_SESSION['_auth']['staff']['2fa'] = null;
         return true;
     }
 
     // If 2fa is set then it means it's pending
     function is2FAPending() {
-        if (!isset($_SESSION['_auth']['staff']['2fa']))
-            return false;
-
-        return true;
-    }
-
-    function isValid(){
-        global $cfg;
-
-        if(!$this->getId() || $this->session->getSessionId()!=session_id())
-            return false;
-
-        if ($this->is2FAPending())
-            return false;
-
-        return $this->session->isvalidSession($this->token,$cfg->getStaffTimeout(),$cfg->enableStaffIPBinding())?true:false;
-    }
-
-    function refreshSession($force=false){
-        global $cfg;
-
-        $time = $this->session->getLastUpdate($this->token);
-        // Deadband session token updates to once / 30-seconds
-        if (!$force && time() - $time < 30)
-            return;
-
-        $this->token=$this->getSessionToken();
-
-        osTicketSession::renewCookie($time, $cfg->getStaffSessionTimeout());
-    }
-
-    function getSession() {
-        return $this->session;
-    }
-
-    function getSessionToken() {
-        return $this->session->sessionToken();
+        return isset($_SESSION['_auth']['staff']['2fa']);
     }
 
-    function getIP(){
-        return $this->session->getIP();
+    function isValid() {
+        return (!$this->is2FAPending() && $this->isValidSession());
     }
-
 }
-
 ?>
diff --git a/open.php b/open.php
index 2c138a76e7..10f0b0a273 100644
--- a/open.php
+++ b/open.php
@@ -45,13 +45,13 @@ if ($_POST) {
     if(($ticket=Ticket::create($vars, $errors, SOURCE))){
         $msg=__('Support ticket request created');
         // Drop session-backed form data
         unset($_SESSION[':form-data']);
         //Logged in...simply view the newly created ticket.
-        if($thisclient && $thisclient->isValid()) {
-            session_regenerate_id();
-            session_write_close();
+        if ($thisclient && $thisclient->isValid()) {
+            // Regenerate session id
+            $thisclient->regenerateSession();
             @header('Location: tickets.php?id='.$ticket->getId());
         } else
             $ost->getCSRF()->rotate();
     }else{
         $errors['err'] = $errors['err'] ?: sprintf('%s %s',

Vulnerable code: 
        $staff->refreshSession(true); //set the hash.


        $user->refreshSession(true); //set the hash.


   var $userID='';
   var $validated=FALSE;

   function __construct($userid){
      $this->browser=(!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
      $this->ip=(!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
      $this->session_id=session_id();
      $this->userID=$userid;
   function isStaff(){
       return FALSE;
       return FALSE;

   function getSessionId(){
   function getIP(){
   function getBrowser(){
   function refreshSession(){
       //nothing to do...clients need to worry about it.
   }


      return($token);
       @list($hash,$expire,$ip)=explode(":",$htoken);
        global $cfg;
        $token = rawurldecode($htoken);
        #check if we got what we expected....
        if($token && !strstr($token,":"))
            return FALSE;
        #get the goodies
        list($hash,$expire,$ip)=explode(":",$token);
        #Make sure the session hash is valid
        if((md5($expire . SESSION_SECRET . $this->userID)!=$hash)){
            return FALSE;
        }
        #is it expired??
        if($maxidletime && ((time()-$expire)>$maxidletime)){
            return FALSE;
        }
        if($checkip && strcmp($ip, MD5($this->ip)))
            return FALSE;
        $this->validated=TRUE;
        return TRUE;
        return FALSE;
class ClientSession extends EndUser {

    function __construct($user) {
        parent::__construct($user);
        $this->token = &$_SESSION[':token']['client'];
        // XXX: Change the key to user-id
        $this->session= new UserSession($user->getId());
    }

    function getSessionUser() {
        return $this->user;
    }

    function isValid(){
        global $_SESSION,$cfg;

        if(!$this->getId() || $this->session->getSessionId()!=session_id())
            return false;

        return $this->session->isvalidSession($this->token,$cfg->getClientTimeout(),false)?true:false;
    }

    function refreshSession($force=false){
        global $cfg;

        //TODO: separate expire time from hash??
        osTicketSession::renewCookie($time, $cfg->getClientSessionTimeout());
    function getIP(){
class StaffSession extends Staff {
    var $session;
    var $token;
        if ($staff = parent::lookup($var)) {
            $staff->token = &$_SESSION[':token']['staff'];
            $staff->session= new UserSession($staff->getId());
        if (!isset($_SESSION['_auth']['staff']['2fa']))
            return false;

        return true;
    }

    function isValid(){
        global $cfg;

        if(!$this->getId() || $this->session->getSessionId()!=session_id())
            return false;

        if ($this->is2FAPending())
            return false;

        return $this->session->isvalidSession($this->token,$cfg->getStaffTimeout(),$cfg->enableStaffIPBinding())?true:false;
    }

    function refreshSession($force=false){
        global $cfg;

        $time = $this->session->getLastUpdate($this->token);
        // Deadband session token updates to once / 30-seconds
        if (!$force && time() - $time < 30)
            return;

        $this->token=$this->getSessionToken();

        osTicketSession::renewCookie($time, $cfg->getStaffSessionTimeout());
    }

    function getSession() {
        return $this->session;
    }

    function getSessionToken() {
        return $this->session->sessionToken();
    function getIP(){
        return $this->session->getIP();


        if($thisclient && $thisclient->isValid()) {
            session_regenerate_id();
            session_write_close();
Fixed code:         // Regenerate session id and refresh (avoid session fixation)
        $staff->regenerateSession();
        // Set session token
        $staff->setSessionToken();
        // Set Auth Key
        // Regenerate session id and refresh (avoid session fixation)
        $user->regenerateSession();
        // Set Session Token
        $user->setSessionToken();
   var $userID = 0;
   var $validated = false;
   function __construct($userid) {
      $this->browser = (!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
      $this->ip = (!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
      $this->session_id = session_id();
      $this->userID = $userid;
   function isStaff() {
       return false;
       return false;
   function getSessionId() {
   function getIP() {
   function getBrowser() {
      return $token;
       @list($hash, $expire, $ip) = explode(":", $htoken);
        global $cfgi;
        // Compare session ids
        if (strcmp($this->getSessionId(), session_id()))
            return false;
        // Is the session invalidated?
        if (isset($_SESSION['KAPUT']) &&  $_SESSION['KAPUT'] < time())
            return (session_destroy() && false);
        $token = rawurldecode($htoken);
        // Check if we got what we expected....
        if ($token && !strstr($token,":"))
            return false;
        // Get the goodies
        list($hash, $expire, $ip) = explode(":",$token);
        // Make sure the session hash is valid
        if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
            return false;

        // is it expired??
        if ($maxidletime && ((time()-$expire)>$maxidletime))
            return false;
        if ($checkip && strcmp($ip, MD5($this->ip)))
            return false;

        $this->validated = true;
        return true;
   }

   function regenerateSession($destroy=false) {
       // Delayed kaput time for current session
       $_SESSION['KAPUT'] = time() + 60;
       // Save the session id as old
       $old = session_id();
       // Regenerate the session without destroying data
       session_regenerate_id(false);
       // Get new session id and close
       $new = session_id();
       session_write_close();
       // Start new session
       session_id($new);
       session_start();
       $this->session_id  = $new;
       // Make sure new session is not set to KAPUT
       unset($_SESSION['KAPUT']);
       // Destroy ?
       if ($destroy) {
           // Destrory old session
           $this->destroySession($old);
           // Restore new session
           session_id($new);
           session_start();
       }
       return true;
   }
   function destroySession($id) {
       // Close current session
       session_write_close();
       // Start target session
       session_id($id);
       session_start();
       // Destroy session
       session_destroy();
       session_write_close();
       return true;
        return  ($this->validated);
trait UserSessionTrait {
    // Session Object
    // Session Token
    // Maximum idle time before session is considered invalid
    var $maxidletime = 0;
    // Indicates if session is bound to the IP address
    var $checkip = false;
    // User class
    var $class = '';

    function refreshSession($force=false) {
        osTicketSession::renewCookie($time, $this->maxidletime);
    }
    function regenerateSession($destroy=false) {
        $this->session->regenerateSession($destroy);
        // Set cookie for the new session id.
        $this->refreshSession(true);
    function setSessionToken($token=null) {
        // Assign memory to token variable
        $this->token = &$_SESSION[':token'][$this->class];
        // Set token
        $this->token = $token ?: $this->getSessionToken();
    }

    function getIP() {

    function isValidSession() {
        return ($this->getId()
                && $this->session->isvalidSession($this->token,
                    $this->maxidletime, $this->checkip));
    }

    abstract function isValid();
class ClientSession extends EndUser {
    use UserSessionTrait;
    function __construct($user) {
        global $cfg;
        parent::__construct($user);
        $this->class ='client';
        // XXX: Change the key to user-id
        $this->session = new UserSession($user->getId());
        $this->setSessionToken();
        $this->maxidletime = $cfg->getClientTimeout();
    }
    function getSessionUser() {
        return $this->user;
    }

    function isValid() {
        return $this->isValidSession();
    }
}

class StaffSession extends Staff {
    use UserSessionTrait;
        global $cfg;
        if (($staff = parent::lookup($var))) {
            $staff->class = 'staff';
            $staff->session = new UserSession($staff->getId());
            $staff->setSessionToken();
            $staff->maxidletime = $cfg->getStaffTimeout();
            $staff->checkip = $cfg->enableStaffIPBinding();
        unset($_SESSION['_auth']['staff']['2fa']);
        return isset($_SESSION['_auth']['staff']['2fa']);
    function isValid() {
        return (!$this->is2FAPending() && $this->isValidSession());
        if ($thisclient && $thisclient->isValid()) {
            // Regenerate session id
            $thisclient->regenerateSession();