Index: branches/5.1.x/core/kernel/session/session.php =================================================================== diff -u -N -r12453 -r12657 --- branches/5.1.x/core/kernel/session/session.php (.../session.php) (revision 12453) +++ branches/5.1.x/core/kernel/session/session.php (.../session.php) (revision 12657) @@ -1,6 +1,6 @@ SetCookieName('my_sid_cookie'); $session->SetGETName('sid'); $session->InitSession(); - ... //link output: @@ -69,7 +72,11 @@ */ -//Implements session storage in the database + +/** + * Implements Session Store in the Database + * + */ class SessionStorage extends kDBBase { var $Expiration; @@ -103,6 +110,29 @@ $this->SessionTimeout = $new_timeout; } + /** + * Calculates browser signature + * + * @return string + */ + function _getBrowserSignature() + { + $signature_parts = Array( + 'HTTP_USER_AGENT', 'SERVER_PROTOCOL', + 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE' + ); + + $ret = ''; + + foreach ($signature_parts as $signature_part) { + if (array_key_exists($signature_part, $_SERVER)) { + $ret .= '&|&' . $_SERVER[$signature_part]; + } + } + + return md5( substr($ret, 3) ); + } + function StoreSession(&$session, $additional_fields = Array()) { if (defined('IS_INSTALL') && IS_INSTALL && !$this->Application->TableFound($this->TableName)) { @@ -111,9 +141,19 @@ $fields_hash = Array ( $this->IDField => $session->SID, - $this->TimestampField => $session->Expiration + $this->TimestampField => $session->Expiration, ); + if (!defined('IS_INSTALL') || !IS_INSTALL) { + // this column was added only in 5.0.1 version, + // so accessing it while database is not upgraded + // will result in admin's inability to login inside + // installator + $fields_hash['BrowserSignature'] = $this->_getBrowserSignature(); + } + + // default values + additional values + values set during this script run + $additional_fields = array_merge($additional_fields, $this->DirectVars); // used 2 times later $fields_hash = array_merge($fields_hash, $additional_fields); $this->Conn->doInsert($fields_hash, $this->TableName); @@ -143,16 +183,29 @@ function LocateSession($sid) { - $query = ' SELECT * FROM '.$this->TableName.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($sid); - $result = $this->Conn->GetRow($query); + $sql = 'SELECT * + FROM ' . $this->TableName . ' + WHERE ' . $this->IDField . ' = ' . $this->Conn->qstr($sid); + $result = $this->Conn->GetRow($sql); if ($result === false) { return false; } - $this->DirectVars = $result; + // perform security checks to ensure, that session is used by it's creator + if ($this->Application->ConfigValue('SessionBrowserSignatureCheck') && ($result['BrowserSignature'] != $this->_getBrowserSignature())) { + return false; + } + if ($this->Application->ConfigValue('SessionIPAddressCheck') && ($result['IpAddress'] != $_SERVER['REMOTE_ADDR'])) { + // most secure, except for cases where NAT (Network Address Translation) + // is used and two or more computers can have same IP address + return false; + } + + $this->DirectVars = $result; $this->Expiration = $result[$this->TimestampField]; + return true; } @@ -238,9 +291,9 @@ unset($this->OriginalData[$var]); } - function GetFromData(&$session, $var) + function GetFromData(&$session, $var, $default = false) { - return getArrayValue($this->OriginalData, $var); + return array_key_exists($var, $this->OriginalData) ? $this->OriginalData[$var] : $default; } function GetExpiredSIDs() @@ -275,7 +328,7 @@ // delete debugger ouputs left of expired sessions foreach ($expired_sids as $expired_sid) { - $debug_file = (defined('WRITEABLE') ? WRITEABLE : FULL_PATH.'/kernel').'/cache/debug_@'.$expired_sid.'@.txt'; + $debug_file = WRITEABLE . '/cache/debug_@' . $expired_sid . '@.txt'; if (file_exists($debug_file)) { @unlink($debug_file); } @@ -397,6 +450,7 @@ define('smGET_ONLY', 3); define('smCOOKIES_AND_GET', 4); + class Session extends kBase { var $Checkers; @@ -420,6 +474,13 @@ var $SessionSet = false; /** + * Session ID is used from GET + * + * @var bool + */ + var $_fromGet = false; + + /** * Enter description here... * * @var SessionStorage @@ -436,16 +497,17 @@ var $Data; /** - * Names of optional session keys (which does not need to be always stored + * Names of optional session keys with their optional values (which does not need to be always stored) * - * @var array + * @var Array */ - var $OptionalData = array(); + var $OptionalData = Array (); - function Session($mode=smAUTO) + function Session($mode = smAUTO) { parent::kBase(); + $this->SetMode($mode); } @@ -536,7 +598,7 @@ $expired_sids = $this->DeleteExpired(); $my_sid_expired = in_array($this->CachedSID, $expired_sids); - if ( ($expired_sids && $my_sid_expired) || ($this->CachedSID && !$this->SessionSet) ) { + if ( ($expired_sids && $my_sid_expired) || ($this->CachedSID && !$this->_fromGet && !$this->SessionSet) ) { $this->RemoveSessionCookie(); // true was here to force new session creation, but I (kostja) used // RemoveCookie a line above, to avoid redirect loop with expired sid @@ -551,81 +613,58 @@ } } + /** + * This is redirect from https to http or via versa + * + * @return bool + */ function IsHTTPSRedirect() { - $http_referer = getArrayValue($_SERVER, 'HTTP_REFERER'); + $http_referer = array_key_exists('HTTP_REFERER', $_SERVER) ? $_SERVER['HTTP_REFERER'] : false; + return ( ( PROTOCOL == 'https://' && preg_match('#http:\/\/#', $http_referer) ) || ( PROTOCOL == 'http://' && preg_match('#https:\/\/#', $http_referer) ) ); } - function CheckReferer($for_cookies=0) + /** + * Helper method for detecting cookie availability + * + * @return bool + */ + function _checkCookieReferer() { - if (!$for_cookies) { - if ( !$this->Application->ConfigValue('SessionReferrerCheck') || $_SERVER['REQUEST_METHOD'] != 'POST') { - return true; - } - } - $path = preg_replace('/admin[\/]{0,1}$/', '', $this->CookiePath); // removing /admin for compatability with in-portal (in-link/admin/add_link.php) + // removing /admin for compatability with in-portal (in-link/admin/add_link.php) + $path = preg_replace('/admin[\/]{0,1}$/', '', $this->CookiePath); $reg = '#^'.preg_quote(PROTOCOL.ltrim($this->CookieDomain, '.').$path).'#'; - return preg_match($reg, getArrayValue($_SERVER, 'HTTP_REFERER') ) || (defined('IS_POPUP') && IS_POPUP); - } - /*function CheckDuplicateCookies() - { - if (isset($_SERVER['HTTP_COOKIE'])) { - $cookie_str = $_SERVER['HTTP_COOKIE']; - $cookies = explode('; ', $cookie_str); - $all_cookies = array(); - foreach ($cookies as $cookie) { - list($name, $value) = explode('=', $cookie); - if (isset($all_cookies[$name])) { - //double cookie name!!! - $this->RemoveCookie($name); - } - else $all_cookies[$name] = $value; - } - } + return preg_match($reg, getArrayValue($_SERVER, 'HTTP_REFERER') ); } - function RemoveCookie($name) - { - $path = $_SERVER['PHP_SELF']; - $path_parts = explode('/', $path); - $cur_path = ''; - setcookie($name, false, null, $cur_path); - foreach ($path_parts as $part) { - $cur_path .= $part; - setcookie($name, false, null, $cur_path); - $cur_path .= '/'; - setcookie($name, false, null, $cur_path); - } - }*/ - function CheckIfCookiesAreOn() { -// $this->CheckDuplicateCookies(); - if ($this->Mode == smGET_ONLY) - { + if ($this->Mode == smGET_ONLY) { //we don't need to bother checking if we would not use it $this->CookiesEnabled = false; return; } + $http_query =& $this->Application->recallObject('HTTPQuery'); - $cookies_on = isset($http_query->Cookie['cookies_on']); // not good here + $cookies_on = array_key_exists('cookies_on', $http_query->Cookie); // not good here $get_sid = getArrayValue($http_query->Get, $this->GETName); - if ($this->IsHTTPSRedirect() && $get_sid) { //Redirect from http to https on different domain + + if ($this->IsHTTPSRedirect() && $get_sid) { // Redirect from http to https on different domain $this->OriginalMode = $this->Mode; $this->SetMode(smGET_ONLY); } if (!$cookies_on || $this->IsHTTPSRedirect()) { //If referer is our server, but we don't have our cookies_on, it's definetly off $is_install = defined('IS_INSTALL') && IS_INSTALL; - if (!$is_install && $this->CheckReferer(1) && !$this->Application->GetVar('admin') && !$this->IsHTTPSRedirect()) { + if (!$is_install && $this->_checkCookieReferer() && !$this->Application->GetVar('admin') && !$this->IsHTTPSRedirect()) { $this->CookiesEnabled = false; } else { @@ -634,8 +673,10 @@ $this->SetCookie('cookies_on', 1, adodb_mktime() + 31104000); //one year should be enough } } - else + else { $this->CookiesEnabled = true; + } + return $this->CookiesEnabled; } @@ -653,53 +694,54 @@ function Check() { - // we should check referer if cookies are disabled, and in combined mode - // auto mode would detect cookies, get only mode would turn it off - so we would get here - // and we don't care about referal in cookies only mode + // don't check referer here, because it doesn't provide any security option and can be easily falsified - if ( $this->Mode != smCOOKIES_ONLY && (!$this->CookiesEnabled || $this->Mode == smCOOKIES_AND_GET) ) { - if (!$this->CheckReferer()) - return false; - } - $sid = $this->GetPassedSIDValue(); - if (empty($sid)) return false; + if (empty($sid)) { + return false; + } //try to load session by sid, if everything is fine $result = $this->LoadSession($sid); - $this->SessionSet = $result; + $this->SessionSet = $result; // fake front-end session will given "false" here return $result; } function LoadSession($sid) { if( $this->Storage->LocateSession($sid) ) { - //if we have session with such SID - get its expiration + // if we have session with such SID - get its expiration $this->Expiration = $this->Storage->GetExpiration(); - //If session has expired + // If session has expired if ($this->Expiration < adodb_mktime()) { $this->Destroy(); // when Destory methods calls SetSession inside and new session get created return $this->SessionSet; } - //Otherwise it's ok + // Otherwise it's ok return true; } - else //fake or deleted due to expiration SID + else { + // fake or deleted due to expiration SID return false; + } } function GetPassedSIDValue($use_cache = 1) { - if (!empty($this->CachedSID) && $use_cache) return $this->CachedSID; + if (!empty($this->CachedSID) && $use_cache) { + return $this->CachedSID; + } + $http_query =& $this->Application->recallObject('HTTPQuery'); $get_sid = getArrayValue($http_query->Get, $this->GETName); + $sid_from_get = $get_sid ? true : false; if ($this->Application->GetVar('admin') == 1 && $get_sid) { $sid = $get_sid; @@ -709,30 +751,37 @@ case smAUTO: //Cookies has the priority - we ignore everything else $sid = $this->CookiesEnabled ? $this->GetSessionCookie() : $get_sid; + + if ($this->CookiesEnabled) { + $sid_from_get = false; + } break; + case smCOOKIES_ONLY: $sid = $this->GetSessionCookie(); break; + case smGET_ONLY: $sid = $get_sid; break; + case smCOOKIES_AND_GET: $cookie_sid = $this->GetSessionCookie(); //both sids should match if cookies are enabled - if (!$this->CookiesEnabled || ($cookie_sid == $get_sid)) - { + if (!$this->CookiesEnabled || ($cookie_sid == $get_sid)) { $sid = $get_sid; //we use get here just in case cookies are disabled } - else - { + else { $sid = ''; + $sid_from_get = false; } break; } } - $this->CachedSID = $sid; + $this->_fromGet = $sid_from_get; + return $this->CachedSID; } @@ -755,18 +804,21 @@ */ function GenerateSID() { - list($usec, $sec) = explode(" ",microtime()); + list ($usec, $sec) = explode(' ', microtime()); $sid_part_1 = substr($usec, 4, 4); - $sid_part_2 = mt_rand(1,9); + $sid_part_2 = mt_rand(1, 9); $sid_part_3 = substr($sec, 6, 4); $digit_one = substr($sid_part_1, 0, 1); + if ($digit_one == 0) { - $digit_one = mt_rand(1,9); + $digit_one = mt_rand(1, 9); $sid_part_1 = preg_replace('/^0/', '', $sid_part_1); - $sid_part_1=$digit_one.$sid_part_1; + $sid_part_1 = $digit_one . $sid_part_1; } - $this->setSID($sid_part_1.$sid_part_2.$sid_part_3); + + $this->setSID($sid_part_1 . $sid_part_2 . $sid_part_3); + return $this->SID; } @@ -778,43 +830,61 @@ */ function setSID($new_sid) { - $this->SID = $this->CachedSID = $new_sid; + $this->SID /*= $this->CachedSID*/ = $new_sid; // don't set cached sid here $this->Application->SetVar($this->GETName,$new_sid); } function NeedSession() { $data = $this->Data->GetParams(); + $data_keys = array_keys($data); - $optional_keys = array_unique($this->OptionalData); + $optional_keys = array_keys($this->OptionalData); $real_keys = array_diff($data_keys, $optional_keys); + return $real_keys ? true : false; } function SetSession($force = false) { - if ($this->SessionSet && !$force) return true; + if ($this->SessionSet && !$force) { + return true; + } + if (!$force && !($this->Application->IsAdmin() || $this->Application->GetVar('admin')) && !$this->NeedSession()) { // don't create session (in db) on Front-End, when sid is present (GPC), but data in db isn't - $this->GenerateSID(); + if ($this->_fromGet) { + // set sid, that was given in GET + $this->setSID( $this->GetPassedSIDValue() ); + } else { + // re-generate sid only, when cookies are used + $this->GenerateSID(); + } return false; } - if (!$this->SID || $force) $this->GenerateSID(); + if (!$this->SID || $force) { + $this->GenerateSID(); + } + $this->Expiration = adodb_mktime() + $this->SessionTimeout; + switch ($this->Mode) { case smAUTO: if ($this->CookiesEnabled) { $this->SetSessionCookie(); } break; + case smGET_ONLY: break; + case smCOOKIES_ONLY: case smCOOKIES_AND_GET: $this->SetSessionCookie(); break; } + $this->Storage->StoreSession($this); if ($this->Application->IsAdmin() || $this->Special == 'admin') { @@ -901,29 +971,38 @@ $this->SID = $this->CachedSID = ''; $this->SessionSet = false; - if ($this->CookiesEnabled) $this->SetSessionCookie(); //will remove the cookie due to value (sid) is empty + if ($this->CookiesEnabled) { + $this->SetSessionCookie(); //will remove the cookie due to value (sid) is empty + } $this->SetSession(true); //will create a new session, true to force } function NeedQueryString($use_cache = 1) { - if ($this->CachedNeedQueryString != null && $use_cache) return $this->CachedNeedQueryString; + if ($this->CachedNeedQueryString != null && $use_cache) { + return $this->CachedNeedQueryString; + } $result = false; - switch ($this->Mode) - { + switch ($this->Mode) { case smAUTO: - if (!$this->CookiesEnabled) $result = true; + if (!$this->CookiesEnabled) { + $result = true; + } break; + /*case smCOOKIES_ONLY: break;*/ + case smGET_ONLY: case smCOOKIES_AND_GET: $result = true; break; } + $this->CachedNeedQueryString = $result; + return $result; } @@ -932,9 +1011,9 @@ $this->Data->AddParams($this->Storage->LoadData($this)); } - function PrintSession($comment='') + function PrintSession($comment = '') { - if($this->Application->isDebugMode() && constOn('DBG_SHOW_SESSIONDATA')) { + if (defined('DEBUG_MODE') && $this->Application->isDebugMode() && constOn('DBG_SHOW_SESSIONDATA')) { // dump session data $this->Application->Debugger->appendHTML('SessionStorage [' . ($this->RecallVar('admin') == 1 ? 'Admin' : 'Front-End') . '] ('.$comment.'):'); $session_data = $this->Data->GetParams(); @@ -945,8 +1024,25 @@ } } $this->Application->Debugger->dumpVars($session_data); + + if (!$this->RecallVar('admin')) { + // dump real keys (only for front-end) + $data_keys = array_keys($session_data); + $optional_keys = array_keys($this->OptionalData); + $real_keys = array_diff($data_keys, $optional_keys); + + if ($real_keys) { + $ret = ''; + foreach ($real_keys as $real_key) { + $ret .= '[' . $real_key . '] = [' . $session_data[$real_key] . ']
'; + } + + $this->Application->Debugger->appendHTML('Real Keys:
' . $ret); + } + } } - if ($this->Application->isDebugMode() && constOn('DBG_SHOW_PERSISTENTDATA')) { + + if (defined('DEBUG_MODE') && $this->Application->isDebugMode() && constOn('DBG_SHOW_PERSISTENTDATA')) { // dump persistent session data if ($this->Storage->PersistentVars) { $this->Application->Debugger->appendHTML('Persistant Session:'); @@ -1019,6 +1115,10 @@ elseif ($this->Application->GetVar('admin')) { // admin checking by session data to prevent recursive session save if (!$this->RecallVar('admin')) { + // bug: we get recursion in this place, when cookies are disabled in browser and we are browsing + // front-end in admin's frame (front-end session is initialized using admin's sid and they are + // mixed together) + $admin_session =& $this->Application->recallObject('Session.admin'); /* @var $admin_session Session */ @@ -1055,6 +1155,11 @@ $params['__URLENCODE__'] = 1; // uses "&" instead of "&" for url part concatenation + replaces "\" to "%5C" (works in HTML) + + if ($this->Application->GetVar('admin') && !array_key_exists('admin', $params) && !defined('EDITING_MODE')) { + $params['editing_mode'] = ''; // used in kApplication::Run + } + $params = array_merge($this->Application->getPassThroughVariables($params), $params); $ret = $this->Application->BuildEnv($t, $params, 'all'); @@ -1068,9 +1173,20 @@ function StoreVar($name, $value, $optional = false) { $this->Data->Set($name, $value); + if ($optional) { - $this->OptionalData[] = $name; + // make variable optional, also remember optional value + $this->OptionalData[$name] = $value; } + elseif (!$optional && array_key_exists($name, $this->OptionalData)) { + if ($this->OptionalData[$name] == $value) { + // same value as optional -> don't remove optional mark + return ; + } + + // make variable non-optional + unset($this->OptionalData[$name]); + } } function StorePersistentVar($name, $value) @@ -1122,7 +1238,14 @@ */ function RestoreVar($name) { - return $this->StoreVar($name, $this->Storage->GetFromData($this, $name)); + $value = $this->Storage->GetFromData($this, $name, '__missing__'); + + if ($value === '__missing__') { + // there is nothing to restore (maybe session was not saved), look in optional variable values + $value = array_key_exists($name, $this->OptionalData) ? $this->OptionalData[$name] : false; + } + + return $this->StoreVar($name, $value); } function GetField($var_name, $default = false) @@ -1162,6 +1285,4 @@ return $ret; } -} - -?> \ No newline at end of file +} \ No newline at end of file