From 44984d6b3e3ee7102d09f7be626e0029564cd56a Mon Sep 17 00:00:00 2001 From: Rick Ellis Date: Wed, 20 Aug 2008 22:26:07 +0000 Subject: Updated the Session class so that if a database is being used, any custom data is stored to the DB rather then the cookie. --- system/libraries/Session.php | 467 +++++++++++++++++++++++-------------------- 1 file changed, 254 insertions(+), 213 deletions(-) (limited to 'system/libraries/Session.php') diff --git a/system/libraries/Session.php b/system/libraries/Session.php index 31a162e81..9907ade47 100644 --- a/system/libraries/Session.php +++ b/system/libraries/Session.php @@ -26,17 +26,24 @@ */ class CI_Session { + var $sess_encrypt_cookie = FALSE; + var $sess_use_database = FALSE; + var $sess_table_name = ''; + var $sess_expiration = 7200; + var $sess_match_ip = FALSE; + var $sess_match_useragent = TRUE; + var $sess_cookie_name = 'ci_session'; + var $cookie_prefix = ''; + var $cookie_path = ''; + var $cookie_domain = ''; + var $sess_time_to_update = 300; + var $encryption_key = ''; + var $flashdata_key = 'flash'; + var $time_reference = 'time'; + var $gc_probability = 5; + var $userdata = array(); var $CI; var $now; - var $encryption = TRUE; - var $use_database = FALSE; - var $session_table = FALSE; - var $sess_length = 7200; - var $sess_cookie = 'ci_session'; - var $userdata = array(); - var $gc_probability = 5; - var $flashdata_key = 'flash'; - var $time_to_update = 300; /** * Session Constructor @@ -44,132 +51,70 @@ class CI_Session { * The constructor runs the session routines automatically * whenever the class is instantiated. */ - function CI_Session() + function CI_Session($params = array()) { - $this->CI =& get_instance(); - log_message('debug', "Session Class Initialized"); - $this->sess_run(); - } - - // -------------------------------------------------------------------- - - /** - * Run the session routines - * - * @access public - * @return void - */ - function sess_run() - { - /* - * Set the "now" time - * - * It can either set to GMT or time(). The pref - * is set in the config file. If the developer - * is doing any sort of time localization they - * might want to set the session time to GMT so - * they can offset the "last_activity" time - * based on each user's locale. - * - */ - if (is_numeric($this->CI->config->item('sess_time_to_update'))) - { - $this->time_to_update = $this->CI->config->item('sess_time_to_update'); - } - - if (strtolower($this->CI->config->item('time_reference')) == 'gmt') - { - $now = time(); - $this->now = mktime(gmdate("H", $now), gmdate("i", $now), gmdate("s", $now), gmdate("m", $now), gmdate("d", $now), gmdate("Y", $now)); - - if (strlen($this->now) < 10) - { - $this->now = time(); - log_message('error', 'The session class could not set a proper GMT timestamp so the local time() value was used.'); - } - } - else - { - $this->now = time(); - } - - /* - * Set the session length - * - * If the session expiration is set to zero in - * the config file we'll set the expiration - * two years from now. - * - */ - $expiration = $this->CI->config->item('sess_expiration'); + // Set the super object to a local variable for use throughout the class + $this->CI =& get_instance(); - if (is_numeric($expiration)) + // Set all the session preferences, which can either be set + // manually via the $params array above or via the config file + foreach (array('sess_encrypt_cookie', 'sess_use_database', 'sess_table_name', 'sess_expiration', 'sess_match_ip', 'sess_match_useragent', 'sess_cookie_name', 'cookie_path', 'cookie_domain', 'sess_time_to_update', 'time_reference', 'cookie_prefix', 'encryption_key') as $key) { - if ($expiration > 0) - { - $this->sess_length = $this->CI->config->item('sess_expiration'); - } - else - { - $this->sess_length = (60*60*24*365*2); - } - } - - // Do we need encryption? - $this->encryption = $this->CI->config->item('sess_encrypt_cookie'); + $this->$key = (isset($params[$key])) ? $params[$key] : $this->CI->config->item($key); + } - if ($this->encryption == TRUE) + // Load the string helper so we can use the strip_slashes() function + $this->CI->load->helper('string'); + + // Do we need encryption? If so, load the encryption class + if ($this->sess_encrypt_cookie == TRUE) { $this->CI->load->library('encrypt'); } - // Are we using a database? - if ($this->CI->config->item('sess_use_database') === TRUE AND $this->CI->config->item('sess_table_name') != '') + // Are we using a database? If so, load it + if ($this->sess_use_database === TRUE AND $this->sess_table_name != '') { - $this->use_database = TRUE; - $this->session_table = $this->CI->config->item('sess_table_name'); $this->CI->load->database(); } - - // Set the cookie name - if ($this->CI->config->item('sess_cookie_name') != FALSE) + + // Set the "now" time. Can either be GMT or server time, based on the + // config prefs. We use this to set the "last activity" time + $this->now = $this->_get_time(); + + // Set the session length. If the session expiration is + // set to zero we'll set the expiration two years from now. + if ($this->sess_expiration == 0) { - $this->sess_cookie = $this->CI->config->item('cookie_prefix').$this->CI->config->item('sess_cookie_name'); + $this->sess_expiration = (60*60*24*365*2); } + + // Set the cookie name + $this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name; - /* - * Fetch the current session - * - * If a session doesn't exist we'll create - * a new one. If it does, we'll update it. - * - */ + // Run the Session routine. If a session doesn't exist we'll + // create a new one. If it does, we'll update it. if ( ! $this->sess_read()) { $this->sess_create(); } else { - // We only update the session every five minutes - if (($this->userdata['last_activity'] + $this->time_to_update) < $this->now) - { - $this->sess_update(); - } + $this->sess_update(); } - // Delete expired sessions if necessary - if ($this->use_database === TRUE) - { - $this->sess_gc(); - } - // Delete 'old' flashdata (from last request) $this->_flashdata_sweep(); // Mark all new flashdata as old (data will be deleted before next request) $this->_flashdata_mark(); + + // Delete expired sessions if necessary + $this->_sess_gc(); + + log_message('debug', "Session routines successfully run"); } // -------------------------------------------------------------------- @@ -183,28 +128,28 @@ class CI_Session { function sess_read() { // Fetch the cookie - $session = $this->CI->input->cookie($this->sess_cookie); + $session = $this->CI->input->cookie($this->sess_cookie_name); + // No cookie? Goodbye cruel world!... if ($session === FALSE) { log_message('debug', 'A session cookie was not found.'); return FALSE; } - // Decrypt and unserialize the data - if ($this->encryption == TRUE) + // Decrypt the cookie data + if ($this->sess_encrypt_cookie == TRUE) { $session = $this->CI->encrypt->decode($session); } else { // encryption was not used, so we need to check the md5 hash - $hash = substr($session, strlen($session)-32); // get last 32 chars + $hash = substr($session, strlen($session)-32); // get last 32 chars $session = substr($session, 0, strlen($session)-32); - // Does the md5 hash match? This is to prevent manipulation of session data - // in userspace - if ($hash !== md5($session.$this->CI->config->item('encryption_key'))) + // Does the md5 hash match? This is to prevent manipulation of session data in userspace + if ($hash !== md5($session.$this->encryption_key)) { log_message('error', 'The session cookie data did not match what was expected. This could be a possible hacking attempt.'); $this->sess_destroy(); @@ -212,68 +157,75 @@ class CI_Session { } } - $session = @unserialize($this->strip_slashes($session)); + // Unserialize the session array + $session = @unserialize(strip_slashes($session)); - if ( ! is_array($session) OR ! isset($session['last_activity'])) + // Is the session data we unserialized and array with the correct format? + if ( ! is_array($session) OR ! isset($session['session_id']) OR ! isset($session['ip_address']) OR ! isset($session['user_agent']) OR ! isset($session['last_activity'])) { - log_message('error', 'The session cookie data did not contain a valid array. This could be a possible hacking attempt.'); + $this->sess_destroy(); return FALSE; } // Is the session current? - if (($session['last_activity'] + $this->sess_length) < $this->now) + if (($session['last_activity'] + $this->sess_expiration) < $this->now) { $this->sess_destroy(); return FALSE; } // Does the IP Match? - if ($this->CI->config->item('sess_match_ip') == TRUE AND $session['ip_address'] != $this->CI->input->ip_address()) + if ($this->sess_match_ip == TRUE AND $session['ip_address'] != $this->CI->input->ip_address()) { $this->sess_destroy(); return FALSE; } // Does the User Agent Match? - if ($this->CI->config->item('sess_match_useragent') == TRUE AND trim($session['user_agent']) != trim(substr($this->CI->input->user_agent(), 0, 50))) + if ($this->sess_match_useragent == TRUE AND trim($session['user_agent']) != trim(substr($this->CI->input->user_agent(), 0, 50))) { $this->sess_destroy(); return FALSE; } // Is there a corresponding session in the DB? - if ($this->use_database === TRUE) + if ($this->sess_use_database === TRUE) { $this->CI->db->where('session_id', $session['session_id']); - if ($this->CI->config->item('sess_match_ip') == TRUE) + if ($this->sess_match_ip == TRUE) { $this->CI->db->where('ip_address', $session['ip_address']); } - if ($this->CI->config->item('sess_match_useragent') == TRUE) + if ($this->sess_match_useragent == TRUE) { $this->CI->db->where('user_agent', $session['user_agent']); } - $query = $this->CI->db->get($this->session_table); + $query = $this->CI->db->get($this->sess_table_name); + // No result? Kill it! if ($query->num_rows() == 0) { $this->sess_destroy(); return FALSE; } - else + + // Is there custom data? If so, add it to the main session array + $row = $query->row(); + if (isset($row->user_data) AND $row->user_data != '') { - $row = $query->row(); - if (($row->last_activity + $this->sess_length) < $this->now) + $custom_data = @unserialize(strip_slashes($row->user_data)); + + if (is_array($custom_data)) { - $this->CI->db->where('session_id', $session['session_id']); - $this->CI->db->delete($this->session_table); - $this->sess_destroy(); - return FALSE; + foreach ($custom_data as $key => $val) + { + $session[$key] = $val; + } } - } + } } // Session is valid! @@ -286,35 +238,61 @@ class CI_Session { // -------------------------------------------------------------------- /** - * Write the session cookie + * Write the session data * * @access public * @return void */ function sess_write() - { - $cookie_data = serialize($this->userdata); + { + // Are we saving custom data to the DB? If not, all we do is update the cookie + if ($this->sess_use_database === FALSE) + { + $this->_set_cookie(); + return; + } + + // We need two copies of the session data array. One will contain any custom data + // that might have been set. The other will contain the data that will be saved to the cookie + $cookie_userdata = $this->userdata; + $custom_userdata = $this->userdata; + + // Before continuing, we need to determine if there is any custom data to deal with. + // Let's determine this by removing the default indexes to see if there's anything left in the array + foreach (array('session_id','ip_address','user_agent','last_activity') as $val) + { + unset($custom_userdata[$val]); + } - if ($this->encryption == TRUE) + // Did we find any custom data? If not, we turn the empty array into a string + // since there's no reason to serialize and store an empty array in the DB + if (count($custom_userdata) === 0) { - $cookie_data = $this->CI->encrypt->encode($cookie_data); + $custom_userdata = ''; } else { - // if encryption is not used, we provide an md5 hash to prevent userside tampering - $cookie_data = $cookie_data . md5($cookie_data.$this->CI->config->item('encryption_key')); + // Before we serialize the custom data array, let's remove that data from the + // main session array since we do not want to save that info to the cookie + foreach (array_keys($custom_userdata) as $val) + { + unset($cookie_userdata[$val]); + } + + // Serialize the custom data array so we can store it + $custom_userdata = serialize($custom_userdata); } + + // Run the update query + $this->CI->db->where('session_id', $this->userdata['session_id']); + $this->CI->db->update($this->sess_table_name, array('last_activity' => $this->userdata['last_activity'], 'user_data' => $custom_userdata)); - setcookie( - $this->sess_cookie, - $cookie_data, - $this->sess_length + time(), - $this->CI->config->item('cookie_path'), - $this->CI->config->item('cookie_domain'), - 0 - ); + // Write the cookie. Notice that we manually pass the cookie data array to the + // _set_cookie() function. Normally that function will store $this->userdata, but + // in this case that array contains custom data, which we do not want in the cookie. + $this->_set_cookie($cookie_userdata); } - + // -------------------------------------------------------------------- /** @@ -330,6 +308,9 @@ class CI_Session { { $sessid .= mt_rand(0, mt_getrandmax()); } + + // To make the session ID even more secure we'll combine it with the user's IP + $sessid .= $this->CI->input->ip_address(); $this->userdata = array( 'session_id' => md5(uniqid($sessid, TRUE)), @@ -339,14 +320,14 @@ class CI_Session { ); - // Save the session in the DB if needed - if ($this->use_database === TRUE) + // Save the data to the DB if needed + if ($this->sess_use_database === TRUE) { - $this->CI->db->query($this->CI->db->insert_string($this->session_table, $this->userdata)); + $this->CI->db->query($this->CI->db->insert_string($this->sess_table_name, $this->userdata)); } // Write the cookie - $this->sess_write(); + $this->_set_cookie(); } // -------------------------------------------------------------------- @@ -358,7 +339,13 @@ class CI_Session { * @return void */ function sess_update() - { + { + // We only update the session every five minutes by default + if (($this->userdata['last_activity'] + $this->sess_time_to_update) >= $this->now) + { + return; + } + // Save the old session id so we know which record to // update in the database if we need it $old_sessid = $this->userdata['session_id']; @@ -367,20 +354,25 @@ class CI_Session { { $new_sessid .= mt_rand(0, mt_getrandmax()); } + + // To make the session ID even more secure we'll combine it with the user's IP + $new_sessid .= $this->CI->input->ip_address(); + + // Turn it into a hash $new_sessid = md5(uniqid($new_sessid, TRUE)); // Update the session data in the session data array $this->userdata['session_id'] = $new_sessid; $this->userdata['last_activity'] = $this->now; - // Update the session in the DB if needed - if ($this->use_database === TRUE) - { - $this->CI->db->query($this->CI->db->update_string($this->session_table, array('last_activity' => $this->now, 'session_id' => $new_sessid), array('session_id' => $old_sessid))); + // Update the session ID and last_activity field in the DB if needed + if ($this->sess_use_database === TRUE) + { + $this->CI->db->query($this->CI->db->update_string($this->sess_table_name, array('last_activity' => $this->now, 'session_id' => $new_sessid), array('session_id' => $old_sessid))); } // Write the cookie - $this->sess_write(); + $this->_set_cookie(); } // -------------------------------------------------------------------- @@ -392,44 +384,27 @@ class CI_Session { * @return void */ function sess_destroy() - { + { + // Kill the session DB row + if ($this->sess_use_database === TRUE AND isset($this->userdata['session_id'])) + { + $this->CI->db->where('session_id', $this->userdata['session_id']); + $this->CI->db->delete($this->sess_table_name); + } + + // Kill the cookie setcookie( - $this->sess_cookie, + $this->sess_cookie_name, addslashes(serialize(array())), ($this->now - 31500000), - $this->CI->config->item('cookie_path'), - $this->CI->config->item('cookie_domain'), + $this->cookie_path, + $this->cookie_domain, 0 ); } // -------------------------------------------------------------------- - /** - * Garbage collection - * - * This deletes expired session rows from database - * if the probability percentage is met - * - * @access public - * @return void - */ - function sess_gc() - { - srand(time()); - if ((rand() % 100) < $this->gc_probability) - { - $expire = $this->now - $this->sess_length; - - $this->CI->db->where("last_activity < {$expire}"); - $this->CI->db->delete($this->session_table); - - log_message('debug', 'Session garbage collection performed.'); - } - } - - // -------------------------------------------------------------------- - /** * Fetch a specific item from the session array * @@ -509,33 +484,6 @@ class CI_Session { $this->sess_write(); } - // -------------------------------------------------------------------- - - /** - * Strip slashes - * - * @access public - * @param mixed - * @return mixed - */ - function strip_slashes($vals) - { - if (is_array($vals)) - { - foreach ($vals as $key=>$val) - { - $vals[$key] = $this->strip_slashes($val); - } - } - else - { - $vals = stripslashes($vals); - } - - return $vals; - } - - // ------------------------------------------------------------------------ /** @@ -646,6 +594,99 @@ class CI_Session { } } + + // -------------------------------------------------------------------- + + /** + * Get the "now" time + * + * @access private + * @return string + */ + function _get_time() + { + if (strtolower($this->time_reference) == 'gmt') + { + $now = time(); + $time = mktime(gmdate("H", $now), gmdate("i", $now), gmdate("s", $now), gmdate("m", $now), gmdate("d", $now), gmdate("Y", $now)); + } + else + { + $time = time(); + } + + return $time; + } + + // -------------------------------------------------------------------- + + /** + * Write the session cookie + * + * @access public + * @return void + */ + function _set_cookie($cookie_data = NULL) + { + if (is_null($cookie_data)) + { + $cookie_data = $this->userdata; + } + + // Serialize the userdata for the cookie + $cookie_data = serialize($cookie_data); + + if ($this->sess_encrypt_cookie == TRUE) + { + $cookie_data = $this->CI->encrypt->encode($cookie_data); + } + else + { + // if encryption is not used, we provide an md5 hash to prevent userside tampering + $cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key); + } + + // Set the cookie + setcookie( + $this->sess_cookie_name, + $cookie_data, + $this->sess_expiration + time(), + $this->cookie_path, + $this->cookie_domain, + 0 + ); + } + + // -------------------------------------------------------------------- + + /** + * Garbage collection + * + * This deletes expired session rows from database + * if the probability percentage is met + * + * @access public + * @return void + */ + function _sess_gc() + { + if ($this->sess_use_database != TRUE) + { + return; + } + + srand(time()); + if ((rand() % 100) < $this->gc_probability) + { + $expire = $this->now - $this->sess_expiration; + + $this->CI->db->where("last_activity < {$expire}"); + $this->CI->db->delete($this->sess_table_name); + + log_message('debug', 'Session garbage collection performed.'); + } + } + } // END Session Class -- cgit v1.2.3-24-g4f1b