1<?php 2/* 3 * e107 website system 4 * 5 * Copyright (C) 2008-2012 e107 Inc (e107.org) 6 * Released under the terms and conditions of the 7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt) 8 * 9 * Session handler 10 * 11 * $URL$ 12 * $Id$ 13 */ 14 15if (!defined('e107_INIT')) 16{ 17 exit; 18} 19 20/** 21 * @package e107 22 * @subpackage e107_handlers 23 * @version $Id$ 24 * @author SecretR 25 * 26 * Dependencies: 27 * - direct: language handler 28 * - indirect: system preferences (required by language handler) 29 * 30 * What could break it? 31 * If session is started before the first system session call (see class2.php 32 * 'Start: Set User Language' phase), session config will not be applied! 33 * This could happen if included $CLASS2_INCLUDE script (see class2.php) 34 * calls session_start(). However, sessions will not be broken, just not secured 35 * as per e_SECURITY_LEVEL setting. 36 * 37 * Security levels: 38 * - SECURITY_LEVEL_NONE [0]: security disabled - no token checks, all session validation settings dsiabled 39 * - SECURITY_LEVEL_BALANCED [5]: ValidateRemoteAddr, ValidateHttpXForwardedFor are on, 40 * session token is created/checked, but not regenerated on every page load 41 * - SECURITY_LEVEL_HIGH [7]: Same as above but ValidateHttpVia, ValidateHttpUserAgent are on. 42 * - SECURITY_LEVEL_PARANOID [9]: Same as SECURITY_LEVEL_HIGH except session token is regenerated on 43 * every page load. 'httponly' is on, which means JS is unable to retrieve session cookie, this may cause 44 * troubles with some browsers. 45 * - SECURITY_LEVEL_INSANE [10]: Same as SECURITY_LEVEL_HIGH plus session id is regenerated at the end 46 * of every page request. 47 * 48 * Session objects are created by namespace: 49 * $_SESSION['e107'] is default namesapce auto created with 50 * <code><?php e107::getSession();</code> 51 * Session handler is validating corresponding session COOKIE 52 * (named as current session name, keeping the session id) 53 * on regular basis (session lifetime/4). If validation 54 * fails, corresponding cookie is destroyed (not the session itself). 55 * 56 * Initial system Session is started after language detection (see class2.php) to 57 * ensure proper session handling for sites using language sub-domains (e.g. fr.site.com) 58 * 59 * Some important system session data will be kept outside of the object for now (e.g. user validation data) 60 * 61 */ 62 63 64class e_session 65{ 66 /** 67 * No protection, label 'Looking for trouble' 68 * @var integer 69 */ 70 const SECURITY_LEVEL_NONE = 0; 71 72 /** 73 * Default system protection, balanced for best user experience, 74 * label 'Safe mode - Balanced' 75 * @var integer 76 */ 77 const SECURITY_LEVEL_BALANCED = 5; 78 79 /** 80 * Adds more system security, but there is a chance (minimal) to break stuff, 81 * label 'High Security' 82 * @var integer 83 */ 84 const SECURITY_LEVEL_HIGH = 7; 85 86 /** 87 * High system protection, session id is regenerated on every page request, 88 * label 'Paranoid' 89 * @var integer 90 */ 91 const SECURITY_LEVEL_PARANOID = 9; 92 93 /** 94 * Highest system protection, session id and token values are regenerated on every page request, 95 * label 'Insane' 96 * @var int unknown_type 97 */ 98 const SECURITY_LEVEL_INSANE = 10; 99 100 /** 101 * Session save path 102 * @var string 103 */ 104 protected $_sessionSavePath = false; 105 106 /** 107 * Session save method 108 * @var string files|db 109 */ 110 protected $_sessionSaveMethod = 'files';//'files'; 111 112 /** 113 * Session cache limiter, ignored if empty 114 * php.net/manual/en/function.session-cache-limiter.php 115 * @var string public|private_no_expire|private|nocache 116 */ 117 protected $_sessionCacheLimiter = ''; 118 119 protected $_namespace; 120 protected $_name; 121 protected $_sessionStarted = false; // Fixes lost $_SESSION value problem. 122 123 /** 124 * Validation options 125 * @var boolean 126 */ 127 protected $_sessionValidateRemoteAddr = true; 128 protected $_sessionValidateHttpVia = true; 129 protected $_sessionValidateHttpXForwardedFor = true; 130 protected $_sessionValidateHttpUserAgent = true; 131 132 /** 133 * Skip validation 134 * @var array 135 */ 136 protected $_sessionValidateRemoteAddrSkip = array(); 137 protected $_sessionValidateHttpViaSkip = array(); 138 protected $_sessionValidateHttpXForwardedForSkip = array(); 139 protected $_sessionValidateHttpUserAgentSkip = array(); 140 141 /** 142 * Default session options 143 * @var array 144 */ 145 protected $_options = array( 146 'lifetime' => 3600 , // 1 hour 147 'path' => '', 148 'domain' => '', 149 'secure' => false, 150 'httponly' => true, 151 ); 152 153 /** 154 * Session data 155 * @var array 156 */ 157 protected $_data = array(); 158 159 /** 160 * Set session options 161 * @param string $key 162 * @param mixed $value 163 * @return e_session 164 */ 165 public function setOption($key, $value) 166 { 167 $this->setOptions(array($key => $value)); 168 return $this; 169 } 170 171 public function getOptions() 172 { 173 return $this->_options; 174 } 175 176 177 178 /** 179 * Get session option 180 * @param string $key 181 * @param mixed $default 182 * @return mixed value 183 */ 184 public function getOption($key, $default = null) 185 { 186 return (isset($this->_options[$key]) ? $this->_options[$key] : $default); 187 } 188 189 /** 190 * Set default settings/options based on the current security level 191 * NOTE: new prefs 'session_save_path', 'session_save_method', 'session_lifetime' introduced, 192 * still not added to preference administration 193 * @return e_session 194 */ 195 public function setDefaultSystemConfig() 196 { 197 if ($this->getSessionId()) return $this; 198 199 $config = array( 200 'ValidateRemoteAddr' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED), 201 'ValidateHttpVia' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH), 202 'ValidateHttpXForwardedFor' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED), 203 'ValidateHttpUserAgent' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH), 204 ); 205 206 $options = array( 207 // 'httponly' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_PARANOID), 208 'httponly' => true, 209 ); 210 211 if (!defined('E107_INSTALL')) 212 { 213 $systemSaveMethod = ini_get('session.save_handler'); 214 215 $saveMethod = (!empty($systemSaveMethod)) ? $systemSaveMethod : 'files'; 216 217 $config['SavePath'] = e107::getPref('session_save_path', false); // FIXME - new pref 218 $config['SaveMethod'] = e107::getPref('session_save_method', $saveMethod); 219 $options['lifetime'] = (integer)e107::getPref('session_lifetime', 86400); 220 $options['path'] = e107::getPref('session_cookie_path', ''); // FIXME - new pref 221 $options['secure'] = e107::getPref('ssl_enabled', false); // 222 223 e107::getDebug()->log("Session Save Method: ".$config['SaveMethod']); 224 225 if (!empty($options['secure'])) 226 { 227 ini_set('session.cookie_secure', 1); 228 } 229 } 230 231 if (defined('SESSION_SAVE_PATH')) // safer than a pref. 232 { 233 $config['SavePath'] = e_BASE . SESSION_SAVE_PATH; 234 } 235 236 $hashes = hash_algos(); 237 238 if ((e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED) && in_array('sha512', $hashes)) 239 { 240 ini_set('session.hash_function', 'sha512'); 241 ini_set('session.hash_bits_per_character', 5); 242 } 243 244 $this->fixSessionFileGarbageCollection(); 245 246 $this->setConfig($config) 247 ->setOptions($options); 248 249 return $this; 250 } 251 252 /** 253 * Modify PHP ini at runtime to enable session file garbage collection 254 * 255 * Takes no action if the garbage collector is already enabled. 256 * 257 * @see https://github.com/e107inc/e107/issues/4113 258 * @return void 259 */ 260 private function fixSessionFileGarbageCollection() 261 { 262 $gc_probability = ini_get('session.gc_probability'); 263 if ($gc_probability > 0) return; 264 265 ini_set('session.gc_probability', 1); 266 ini_set('session.gc_divisor', 100); 267 } 268 269 /** 270 * Retrieve value from current session namespace 271 * Equals to $_SESSION[NAMESPACE][$key] 272 * @param string $key 273 * @param boolean $clear unset key 274 * @return mixed 275 */ 276 public function get($key, $clear = false) 277 { 278 $ret = isset($this->_data[$key]) ? $this->_data[$key] : null; 279 if($clear) $this->clear($key); 280 return $ret; 281 } 282 283 /** 284 * Retrieve value from current session namespace 285 * If key is null, returns all current session namespace data 286 * 287 * @param string|null $key 288 * @param boolean $clear 289 * @return mixed 290 */ 291 public function getData($key = null, $clear = false) 292 { 293 if(null === $key) 294 { 295 $ret = $this->_data; 296 if($clear) $this->clearData(); 297 return $ret; 298 } 299 return $this->get($key, $clear); 300 } 301 302 /** 303 * Set value in current session namespace 304 * Equals to $_SESSION[NAMESPACE][$key] = $value 305 * @param string $key 306 * @param mixed $value 307 * @return e_session 308 */ 309 public function set($key, $value) 310 { 311 $this->_data[$key] = $value; 312 return $this; 313 } 314 315 /** 316 * Set value in current session namespace 317 * If $key is array, the whole namespace array will be replaced with it, 318 * $value will be ignored 319 * @param string|null $key 320 * @param mixed $value 321 * @return e_session 322 */ 323 public function setData($key, $value = null) 324 { 325 if(is_array($key)) 326 { 327 $this->_data = $key; 328 return $this; 329 } 330 return $this->set($key, $value); 331 } 332 333 /** 334 * Check if given key is set in current session namespace 335 * Equals to isset($_SESSION[NAMESPACE][$key]) 336 * @param string $key 337 * @return boolean 338 */ 339 public function is($key) 340 { 341 return isset($this->_data[$key]); 342 } 343 344 /** 345 * Check if given key is set and not empty in current session namespace 346 * Equals to !empty($_SESSION[NAMESPACE][$key]) check 347 * @param string $key 348 * @return boolean 349 */ 350 public function has($key) 351 { 352 return (isset($this->_data[$key]) && $this->_data[$key]); 353 } 354 355 /** 356 * Checks if current session namespace contains any data 357 * Equals to !empty($_SESSION[NAMESPACE]) check 358 * @return boolean 359 */ 360 public function hasData() 361 { 362 return !empty($this->_data); 363 } 364 365 /** 366 * Unset member of current session namespace array 367 * Equals to unset($_SESSION[NAMESPACE][$key]) 368 * @param string $key 369 * @return e_session 370 */ 371 public function clear($key=null) 372 { 373 if($key == null) // clear all under this namespace. 374 { 375 $this->_data = array(); // must be set to array() not unset. 376 } 377 378 unset($this->_data[$key]); 379 return $this; 380 } 381 382 /** 383 * Reset current session namespace to empty array 384 * @return e_session 385 */ 386 public function clearData() 387 { 388 $this->_data = array(); 389 return $this; 390 } 391 392 /** 393 * Set protected class vars, prefixed with _session 394 * @param array $config 395 * @return e_session 396 */ 397 public function setConfig($config) 398 { 399 foreach ($config as $k => $v) 400 { 401 $key = '_session'.$k; 402 if (isset($this->$key)) $this->$key = $v; 403 } 404 return $this; 405 } 406 407 /** 408 * Get registered namespace key 409 * @return string 410 */ 411 public function getNamespaceKey() 412 { 413 return $this->_namespace; 414 } 415 416 /** 417 * Reset session options 418 * @param array $options 419 * @return e_session 420 */ 421 public function setOptions($options) 422 { 423 if (empty($options) || !is_array($options)) return $this; 424 foreach ($options as $k => $v) 425 { 426 switch ($k) 427 { 428 case 'lifetime': 429 $v = intval($v); 430 break; 431 432 case 'path': 433 case 'domain': 434 $v = (string) $v; 435 break; 436 437 case 'secure': 438 case 'httponly': 439 $v = $v ? true : false; 440 break; 441 442 default: 443 $v = null; 444 break; 445 } 446 447 if($v !== null) 448 { 449 $this->_options[$k] = $v; 450 } 451 } 452 return $this; 453 } 454 455 public function init($namespace, $sessionName = null) 456 { 457 $this->start($sessionName); 458 459 if (!isset($_SESSION[$namespace])) 460 { 461 $_SESSION[$namespace] = array(); 462 } 463 $this->_data =& $_SESSION[$namespace]; 464 $this->_namespace = $namespace; 465 466 $this->validate(); 467 $this->validateSessionCookie(); 468 } 469 470 /** 471 * Conigure and start session 472 * 473 * @param string $sessionName optional session name 474 * @return e_session 475 */ 476 public function start($sessionName = null) 477 { 478 479 if (isset($_SESSION) && ($this->_sessionStarted == true)) 480 { 481 return $this; 482 } 483 484 if (false !== $this->_sessionSavePath && is_writable($this->_sessionSavePath)) 485 { 486 session_save_path($this->_sessionSavePath); 487 } 488 489 switch ($this->_sessionSaveMethod) 490 { 491 case 'db': 492 ini_set('session.save_handler', 'user'); 493 $session = new e_session_db; 494 $session->setSaveHandler(); 495 break; 496 497 default: 498 if(!isset($_SESSION)) 499 { 500 session_module_name($this->_sessionSaveMethod); 501 } 502 break; 503 } 504 505 if (empty($this->_options['domain'])) 506 { 507 // MULTILANG_SUBDOMAIN set during initial language detection in language handler 508 $doma = ((deftrue('e_SUBDOMAIN') || deftrue('MULTILANG_SUBDOMAIN')) && e_DOMAIN != FALSE) ? ".".e_DOMAIN : FALSE; // from v1.x 509 $this->_options['domain'] = $doma; 510 } 511 512 if (empty($this->_options['path'])) 513 { 514 if(defined('e_MULTISITE_MATCH')) // multisite support. 515 { 516 $this->_options['path'] = '/'; 517 } 518 else 519 { 520 $this->_options['path'] = defined('e_HTTP') ? e_HTTP : '/'; 521 } 522 } 523 524 // session name before options - problems reported on php.net 525 if (!empty($sessionName)) 526 { 527 $this->setSessionName($sessionName); 528 } 529 530 // set session cookie params 531 session_set_cookie_params($this->_options['lifetime'], 532 $this->_options['path'], 533 $this->_options['domain'], 534 $this->_options['secure'], 535 $this->_options['httponly']); 536 537 if ($this->_sessionCacheLimiter) 538 { 539 session_cache_limiter((string) $this->_sessionCacheLimiter); //XXX Remove and have e_headers class handle it? 540 } 541 542 543 session_start(); 544 $this->_sessionStarted = true; 545 return $this; 546 } 547 548 /** 549 * Set session ID 550 * @param string $sid 551 * @return e_session 552 */ 553 public function setSessionId($sid = null) 554 { 555 // comma and minus allowed since 5.0 556 if (!empty($sid) && preg_match('#^[0-9a-zA-Z,-]+$#', $sid)) 557 { 558 session_id($sid); 559 } 560 return $this; 561 } 562 563 /** 564 * Retrieve current session id 565 * @return string 566 */ 567 public function getSessionId() 568 { 569 return session_id(); 570 } 571 572 /** 573 * Retrieve current session save method. 574 * @return string 575 */ 576 public function getSaveMethod() 577 { 578 return $this->_sessionSaveMethod; 579 } 580 581 /** 582 * Set new session name 583 * @param string $name alphanumeric characters only 584 * @return string old session name or false on error 585 */ 586 public function setSessionName($name) 587 { 588 if (!empty($name) && preg_match('#^[0-9a-z_]+$#i', $name)) 589 { 590 $this->_name = $name; 591 return session_name($name); 592 } 593 return false; 594 } 595 596 /** 597 * Retrieve current session name 598 * @return string 599 */ 600 public function getSessionName() 601 { 602 return session_name(); 603 } 604 605 /** 606 * Reset session cookie lifetime 607 * We reset session cookie on every (session_lifetime / 4) seconds 608 * It's done by all session handler instances, they all share 609 * one and the same '_cookie_session_validate' variable (global session namespace) 610 * @return e_session 611 */ 612 public function validateSessionCookie() 613 { 614 if (!$this->_options['lifetime']) 615 { 616 return $this; 617 } 618 619 if (empty($_SESSION['_cookie_session_validate'])) 620 { 621 $time = time() + round($this->_options['lifetime'] / 4); 622 $_SESSION['_cookie_session_validate'] = $time; 623 } 624 elseif ($_SESSION['_cookie_session_validate'] < time()) 625 { 626 if (!headers_sent()) 627 { 628 cookie(session_name(), session_id(), time() + $this->_options['lifetime'], $this->_options['path'], $this->_options['domain'], $this->_options['secure']); 629 $time = time() + round($this->_options['lifetime'] / 4); 630 $_SESSION['_cookie_session_validate'] = $time; 631 } 632 } 633 634 return $this; 635 } 636 637 /** 638 * Delete session cookie 639 * @return e_session 640 */ 641 public function cookieDelete() 642 { 643 cookie(session_name(), null, null, $this->_options['path'], $this->_options['domain'], $this->_options['secure']); 644 return $this; 645 } 646 647 /** 648 * Validate current session 649 * @return e_session 650 */ 651 public function validate() 652 { 653 if (!isset($this->_data['_session_validate_data'])) 654 { 655 $this->_data['_session_validate_data'] = $this->getValidateData(); 656 } 657 elseif (!$this->_validate()) 658 { 659 $sessionData = $this->_data['_session_validate_data']; 660 $validateData = $this->getValidateData(); 661 662 $details = 'USER INFORMATION: '.(isset($_COOKIE[e_COOKIE]) ? $_COOKIE[e_COOKIE] : (isset($_SESSION[e_COOKIE]) ? $_SESSION[e_COOKIE] : 'n/a'))."\n"; 663 $details .= "HOST: ".$_SERVER['HTTP_HOST']."\n"; 664 $details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n"; 665 $details .= "SESSION OPTIONS: ".print_r($this->_options, true)."\n"; 666 $details .= "SESSION NAMESPACE: ".$this->_namespace."\n"; 667 $details .= "SESSION VALIDATION DATA SAVED: ".print_r($sessionData, true)."\n"; 668 $details .= "SESSION VALIDATION DATA CURRENT: ".print_r($validateData, true)."\n"; 669 $details .= "CURRENT NAMESPACE SESSION DATA:\n"; 670 $this->clear('_session_validate_data'); // already logged 671 $details .= print_r($this->_data, true); 672 $this->close(false); 673 $details .= "SESSION GLOBAL DATA:\n"; 674 $details .= print_r($_SESSION, true); 675 676 // delete cookie, destroy session 677 $this->cookieDelete()->destroy(); 678 679 // TODO event trigger 680 681 // e107::getAdminLog()->log_event('Session validation failed!', $details, E_LOG_FATAL); 682 // TODO session exception, handle it proper on live site 683 // throw new Exception(''); 684 685 // just for now 686 $msg = 'Session validation failed! <a href="'.strip_tags($_SERVER['REQUEST_URI']).'">Go Back</a>'; 687 // die($msg); //FIXME not functioning as intended. 688 } 689 690 return $this; 691 } 692 693 /** 694 * Validate current session based on config options 695 * 696 * @return bool 697 */ 698 protected function _validate() 699 { 700 $sessionData = $this->_data['_session_validate_data']; 701 $validateData = $this->getValidateData(); 702 $keyvar = '_sessionValidate'; 703 704 foreach ($validateData as $vkey => $value) 705 { 706 $var = $keyvar.$vkey; 707 $varskip = $var.'Skip'; 708 if ($this->$var && $sessionData[$vkey] != $value && !in_array($value, $this->$varskip)) 709 { 710 return false; 711 } 712 } 713 714 return true; 715 } 716 717 /** 718 * Retrieve data for validator 719 * @return array 720 */ 721 public function getValidateData() 722 { 723 $data = array( 724 'RemoteAddr' => '', 725 'HttpVia' => '', 726 'HttpXForwardedFor' => '', 727 'HttpUserAgent' => '' 728 ); 729 730 // collect ip data 731 if (isset($_SERVER['REMOTE_ADDR'])) 732 { 733 $data['RemoteAddr'] = (string) $_SERVER['REMOTE_ADDR']; 734 } 735 if (isset($_ENV['HTTP_VIA'])) 736 { 737 $data['HttpVia'] = (string) $_ENV['HTTP_VIA']; 738 } 739 if (isset($_ENV['HTTP_X_FORWARDED_FOR'])) 740 { 741 $data['HttpXForwardedFor'] = (string) $_ENV['HTTP_X_FORWARDED_FOR']; 742 } 743 744 // collect user agent data 745 if (isset($_SERVER['HTTP_USER_AGENT'])) 746 { 747 $data['HttpUserAgent'] = (string) $_SERVER['HTTP_USER_AGENT']; 748 } 749 750 return $data; 751 } 752 753 /** 754 * Retrieve (create if doesn't exist) XSF protection token 755 * @param boolean $in_form if true (default) - value for forms, else raw session value 756 * @return string 757 */ 758 public function getFormToken($in_form = true) 759 { 760 if(!$this->has('__form_token') && !defined('e_TOKEN_DISABLE')) // TODO FIXME: SEF URL of Error page causes e-token refresh. 761 { 762 $this->set('__form_token', uniqid(md5(rand()), true)); 763 if(deftrue('e_DEBUG_SESSION')) // XXX enable to troubleshoot "Unauthorized Access!" issues. 764 { 765 $message = date('r')."\t\t".e_REQUEST_URI."\n"; 766 file_put_contents(__DIR__.'/session.log', $message, FILE_APPEND); 767 } 768 } 769 return ($in_form ? md5($this->get('__form_token')) : $this->get('__form_token')); 770 } 771 772 /** 773 * Regenerate form token value 774 * TODO - save old token 775 * @return e_session 776 */ 777 protected function _regenerateFormToken() 778 { 779 $this->set('__form_token', uniqid(md5(rand()), true)); 780 return $this; 781 } 782 783 /** 784 * Do a check against passed token 785 * @param string $token 786 * @return boolean 787 */ 788 public function checkFormToken($token) 789 { 790 $utoken = $this->getFormToken(false); 791 return ($token === md5($utoken)); 792 } 793 794 /** 795 * Clear and Unset current namespace, unregister session singleton 796 * e107::getSession('namespace') if needed. 797 * @param boolean $unregister if true (default) - unregister Singleton, destroy namespace, 798 * else alias of self::clearData() 799 * @return void 800 */ 801 public function close($unregister = true) 802 { 803 $this->clearData(); 804 if($unregister) 805 { 806 unset($_SESSION[$this->_namespace]); 807 e107::setRegistry('core/e107/session/'.$this->_namespace, null); 808 } 809 } 810 811 /** 812 * Save session data to disk, end session. 813 * Sessions can't be used after this point. 814 * Method should be called before every header redirect. 815 * @return void 816 */ 817 public function end() 818 { 819 session_write_close(); 820 } 821 822 /** 823 * Destroy all session data 824 * @return e_session 825 */ 826 public function destroy() 827 { 828 $this->cookieDelete()->close(); 829 //unset($_SESSION); 830 831 // cleanup 832 cookie(e_COOKIE, null, null); // remove user auth cookie 833 // unset($_SESSION['_cookie_session_validate']); 834 835 session_destroy(); 836 return $this; 837 } 838 839 public function replaceRegistry() 840 { 841 e107::setRegistry('core/e107/session/'.$this->_namespace, $this, true); 842 } 843} 844 845class e_core_session extends e_session 846{ 847 /** 848 * Constructor 849 * 3rd party code and/or other system areas are 850 * able to extend the base e_session class and 851 * add more or override the implemented functionality, has their own 852 * namespace, add more session security etc. 853 * @param array $data session config data 854 */ 855 public function __construct($data = array()) 856 { 857 // default system configuration 858 $this->setDefaultSystemConfig(); 859 860 $namespace = 'e107sess'; // Quick Fix for Fatal Error "Cannot use object of type e107 as array" on line 550 861 $name = (isset($data['name']) && !empty($data['name']) ? $data['name'] : deftrue('e_COOKIE', 'e107')).'SID'; 862 if(isset($data['namespace']) && !empty($data['namespace'])) $namespace = $data['namespace']; 863 864 // create $_SESSION['e107'] namespace by default 865 $this->init($namespace, $name); 866 } 867 868 /** 869 * Session shutdown - called at the top of footer_default.php by default 870 * @return void 871 */ 872 public function shutdown() 873 { 874 if(!session_id()) // someone closed the session? 875 { 876 $this->init($this->_namespace, $this->_name); // restart 877 } 878 879 // give 3rd party code a way to prevent token re-generation 880 if(e_SECURITY_LEVEL >= e_session::SECURITY_LEVEL_PARANOID && !deftrue('e_TOKEN_FREEZE')) 881 { 882 if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE) 883 { 884 // regenerate SID 885 $oldSID = session_id(); // old SID 886 $oldSData = $_SESSION; // old session data 887 session_regenerate_id(false); // true don't work on php4 - so time to move on people! 888 $newSID = session_id(); // new SID 889 890 // Clean 891 session_id($oldSID); // switch to the old session 892 session_destroy(); // destroy it 893 894 // set new ID, reopen the session, set saved data 895 session_id($newSID); 896 session_start(); 897 $_SESSION = $oldSData; 898 } 899 $this->set('__form_token_regenerate', time()); // check() needs it to re-create token on the next request 900 } 901 // write session data 902 $this->end(); 903 } 904 905 private function log($status, $type=E_LOG_FATAL) 906 { 907 908 if(!deftrue('e_DEBUG_SESSION')) 909 { 910 return null; 911 } 912 913 914 // $details = "USER: ".USERNAME."\n"; 915 $details = "HOST: ".$_SERVER['HTTP_HOST']."\n"; 916 $details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n"; 917 918 $details .= ($_POST['e-token']) ? "e-token (POST): ".$_POST['e-token']."\n" : ""; 919 $details .= ($_GET['e-token']) ? "e-token (GET): ".$_GET['e-token']."\n" : ""; 920 $details .= ($_POST['e_token']) ? "AJAX e_token (POST): ".$_POST['e_token']."\n" : ""; 921/* 922 $utoken = $this->getFormToken(false); 923 $details .= "raw token: ".$utoken."\n"; 924 $details .= "checkFormToken (e-token should match this): ".md5($utoken)."\n"; 925 $details .= "md5(e-token): ".md5($_POST['e-token'])."\n";*/ 926/* 927 $regenerate = $this->get('__form_token_regenerate'); 928 $details .= "Regenerate after: ".date('r', $regenerate)." (".$regenerate.")\n"; 929*/ 930 931 $details .= "has __form_token: "; 932 $hasToken = $this->has('__form_token'); 933 $details .= empty($hasToken) ? 'false' : 'true'; 934 $details .= "\n"; 935 936 $details .= "_SESSION:\n"; 937 $details .= print_r($_SESSION,true); 938 939 /* if($pref['plug_installed']) 940 { 941 $details .= "\nPlugins:\n"; 942 $details .= print_r($pref['plug_installed'],true); 943 }*/ 944 945 $details .= $status."\n\n---------------------------------\n\n"; 946 947 $log = e107::getAdminLog(); 948 $log->addDebug($details); 949 950 if(deftrue('e_DEBUG_SESSION')) 951 { 952 $log->toFile('Unauthorized_access','Unauthorized access Log', true); 953 } 954 955 $log->add($status, $details, $type); 956 957 958 } 959 /** 960 * Core CSF protection, see class2.php 961 * Could be adopted by plugins for their own (different) protection logic 962 * @param boolean $die 963 * @return boolean 964 */ 965 public function check($die = true) 966 { 967 // define('e_TOKEN_NAME', 'e107_token_'.md5($_SERVER['HTTP_HOST'].e_HTTP)); 968 // TODO e-token required for all system forms? 969 970 // only if not disabled and not in 'cli' mod 971 if(e_SECURITY_LEVEL < e_session::SECURITY_LEVEL_BALANCED || e107::getE107('cli')) return true; 972 973 if($this->getSessionId()) 974 { 975 976 if((isset($_POST['e-token']) && !$this->checkFormToken($_POST['e-token'])) 977 || (isset($_GET['e-token']) && !$this->checkFormToken($_GET['e-token'])) 978 || (isset($_POST['e_token']) && !$this->checkFormToken($_POST['e_token']))) // '-' is not allowed in jquery. b 979 { 980 $this->log('Unauthorized access!'); 981 // do not redirect, prevent dead loop, save server resources 982 if($die == true) 983 { 984 die('Unauthorized access!'); 985 } 986 987 return false; 988 } 989 990 $this->log('Session Token Okay!', E_LOG_NOTICE); 991 992 } 993 994 if(!defined('e_TOKEN')) 995 { 996 // FREEZE token regeneration if minimal, ajax or iframe (ajax and iframe not implemented yet) request 997 $_toFreeze = (e107::getE107('minimal') || e107::getE107('ajax') || e107::getE107('iframe')); 998 if(!defined('e_TOKEN_FREEZE') && $_toFreeze) 999 { 1000 define('e_TOKEN_FREEZE', true); 1001 } 1002 // __form_token_regenerate set in footer, so if footer is not called, token will be never regenerated! 1003 if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE && !deftrue('e_TOKEN_FREEZE') && $this->has('__form_token_regenerate')) 1004 { 1005 $this->_regenerateFormToken() 1006 ->clear('__form_token_regenerate'); 1007 } 1008 define('e_TOKEN', $this->getFormToken()); 1009 } 1010 1011 return true; 1012 } 1013 1014 1015 1016 /** 1017 * Manually Reset the Token. 1018 * @see e107forum::ajaxQuickReply(); 1019 */ 1020 public function reset() 1021 { 1022 $this->_regenerateFormToken()->clear('__form_token_regenerate'); 1023 } 1024 1025 1026 /** 1027 * Make sure there is unique challenge string for CHAP login 1028 * @see class2.php 1029 * @return e_core_session 1030 1031 @TODO: Remove debug code 1032 */ 1033 public function challenge() 1034 { 1035 if (!$this->is('challenge')) // TODO: Eliminate need for this 1036 { 1037 $this->set('challenge', sha1(time().rand().$this->getSessionId())); // New challenge for next time 1038 } 1039 if ($this->is('challenge')) 1040 { 1041 $this->set('prevprevchallenge', $this->get('prevchallenge')); // Purely for debug 1042 $this->set('prevchallenge', $this->get('challenge')); // Need to check user login against this 1043 } 1044 else 1045 { 1046 $this->set('prevchallenge', ''); // Dummy value 1047 $this->set('prevprevchallenge', ''); // Dummy value 1048 } 1049 //$this->set('challenge', sha1(time().rand().$this->getSessionId())); // Temporarily disabled 1050 // FIXME - session id will be regenerated if e_SECURITY_LEVEL is 'paranoid|insane' - generate (might be OK as long as values retained) 1051 1052 //$extra_text = 'C: '.$this->get('challenge').' PC: '.$this->get('prevchallenge').' PPC: '.$this->get('prevprevchallenge'); 1053 //$logfp = fopen(e_LOG.'authlog.txt', 'a+'); fwrite($logfp, strftime('%H:%M:%S').' CHAP start: '.$extra_text."\n"); fclose($logfp); 1054 1055 // could go, see _validate() 1056 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; 1057 $ubrowser = md5('E107'.$user_agent); 1058 if (!$this->is('ubrowser')) 1059 { 1060 $this->set('ubrowser', $ubrowser); 1061 } 1062 return $this; 1063 } 1064} 1065 1066 1067class e_session_db 1068{ 1069 /** 1070 * @var e_db 1071 */ 1072 protected $_db = null; 1073 1074 /** 1075 * Table name 1076 * @var string 1077 */ 1078 protected $_table = 'session'; 1079 1080 /** 1081 * @var integer 1082 */ 1083 protected $_lifetime = null; 1084 1085 public function __construct() 1086 { 1087 $this->_db = e107::getDb('session'); 1088 } 1089 1090 public function __destruct() 1091 { 1092 session_write_close(); 1093 } 1094 1095 /** 1096 * @return string 1097 */ 1098 public function getTable() 1099 { 1100 return $this->_table; 1101 } 1102 1103 /** 1104 * @param string $table 1105 * @return e_session_db 1106 */ 1107 public function setTable($table) 1108 { 1109 $this->_table = $table; 1110 return $this; 1111 } 1112 1113 /** 1114 * @return integer 1115 */ 1116 public function getLifetime() 1117 { 1118 if(null === $this->_lifetime) 1119 { 1120 $this->_lifetime = ini_get('session.gc_maxlifetime'); 1121 if(!$this->_lifetime) 1122 { 1123 $this->_lifetime = 3600; 1124 } 1125 } 1126 return (integer) $this->_lifetime; 1127 } 1128 1129 /** 1130 * @param integer $seconds 1131 * @return e_session_db 1132 */ 1133 public function setLifetime($seconds = null) 1134 { 1135 $this->_lifetime = $seconds; 1136 return $this; 1137 } 1138 1139 /** 1140 * Set session save handler 1141 * @return e_session_db 1142 */ 1143 public function setSaveHandler() 1144 { 1145 session_set_save_handler( 1146 array($this, 'open'), 1147 array($this, 'close'), 1148 array($this, 'read'), 1149 array($this, 'write'), 1150 array($this, 'destroy'), 1151 array($this, 'gc') 1152 ); 1153 return $this; 1154 } 1155 1156 /** 1157 * Open session, parameters are ignored (see e_session handler) 1158 * @param string $save_path 1159 * @param string $sess_name 1160 * @return boolean 1161 */ 1162 public function open($save_path, $sess_name) 1163 { 1164 return true; 1165 } 1166 1167 /** 1168 * Close session 1169 * @return boolean 1170 */ 1171 public function close() 1172 { 1173 $this->gc($this->getLifetime()); 1174 return true; 1175 } 1176 1177 /** 1178 * Get session data 1179 * @param string $session_id 1180 * @return string 1181 */ 1182 public function read($session_id) 1183 { 1184 $data = false; 1185 $check = $this->_db->select($this->getTable(), 'session_data', "session_id='".$this->_sanitize($session_id)."' AND session_expires>".time()); 1186 if($check) 1187 { 1188 $tmp = $this->_db->fetch(); 1189 $data = base64_decode($tmp['session_data']); 1190 } 1191 elseif(false !== $check) 1192 { 1193 $data = ''; 1194 } 1195 return $data; 1196 } 1197 1198 /** 1199 * Write session data 1200 * @param string $session_id 1201 * @param string $session_data 1202 * @return boolean 1203 */ 1204 public function write($session_id, $session_data) 1205 { 1206 $data = array( 1207 'data' => array( 1208 'session_expires' => time() + $this->getLifetime(), 1209 'session_data' => base64_encode($session_data), 1210 ), 1211 '_FIELD_TYPES' => array( 1212 'session_id' => 'str', 1213 'session_expires' => 'int', 1214 'session_data' => 'str' 1215 ), 1216 '_DEFAULT' => 'str' 1217 ); 1218 if(!($session_id = $this->_sanitize($session_id))) 1219 { 1220 return false; 1221 } 1222 1223 $check = $this->_db->select($this->getTable(), 'session_id', "`session_id`='{$session_id}'"); 1224 1225 if($check) 1226 { 1227 $data['WHERE'] = "`session_id`='{$session_id}'"; 1228 if(false !== $this->_db->update($this->getTable(), $data)) 1229 { 1230 return true; 1231 } 1232 } 1233 else 1234 { 1235 $data['data']['session_id'] = $session_id; 1236 if($this->_db->insert($this->getTable(), $data)) 1237 { 1238 return true; 1239 } 1240 } 1241 return false; 1242 } 1243 1244 /** 1245 * Destroy session 1246 * @param string $session_id 1247 * @return boolean 1248 */ 1249 public function destroy($session_id) 1250 { 1251 $session_id = $this->_sanitize($session_id); 1252 $this->_db->delete($this->getTable(), "`session_id`='{$session_id}'"); 1253 return true; 1254 } 1255 1256 /** 1257 * Garbage collection 1258 * @param integer $session_maxlf ignored - see write() 1259 * @return boolean 1260 */ 1261 public function gc($session_maxlf) 1262 { 1263 $this->_db->delete($this->getTable(), '`session_expires`<'.time()); 1264 return true; 1265 } 1266 1267 /** 1268 * Allow only well formed session id string 1269 * @param string $session_id 1270 * @return string 1271 */ 1272 protected function _sanitize($session_id) 1273 { 1274 return preg_replace('#[^0-9a-zA-Z,-]#', '', $session_id); 1275 } 1276} 1277