diff --git a/addons/autolink/autolink.js b/addons/autolink/autolink.js index b2c5534c5..d2affb61d 100644 --- a/addons/autolink/autolink.js +++ b/addons/autolink/autolink.js @@ -36,7 +36,7 @@ var content = textNode.nodeValue; var dummy = $(''); - content = content.replace(//g, '>'); + content = content.escape(); content = content.replace(url_regex, function(match, p1, offset, string) { var match; var suffix = ''; @@ -107,5 +107,5 @@ $this.attr("target", "_blank"); } }); - + })(jQuery); diff --git a/addons/photoswipe/rx_photoswipe.js b/addons/photoswipe/rx_photoswipe.js index d21fbabce..fc6f30d0d 100644 --- a/addons/photoswipe/rx_photoswipe.js +++ b/addons/photoswipe/rx_photoswipe.js @@ -182,7 +182,7 @@ var initPhotoSwipeFromDOM = function(gallerySelector) { captionEl.children[0].innerText = ''; return false; } - captionEl.children[0].innerHTML = item.title; + captionEl.children[0].innerText = item.title; return true; }, diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index a24ed24e3..6d4b65f75 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -347,7 +347,6 @@ class Context if (self::$_current_request->getRouteOption('enable_session')) { session_cache_limiter(''); - Rhymix\Framework\Session::checkSSO($site_module_info); Rhymix\Framework\Session::start(false); } if (self::$_current_request->getRouteOption('cache_control')) diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index 1d8acd7fe..53d3bca1d 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -1355,6 +1355,7 @@ class ModuleHandler extends Handler catch (Rhymix\Framework\Exception $e) { $output = new BaseObject(-2, $e->getMessage()); + $output->add('rx_error_location', $e->getUserFileAndLine()); } if ($trigger_name !== 'common.flushDebugInfo') @@ -1391,6 +1392,7 @@ class ModuleHandler extends Handler catch (Rhymix\Framework\Exception $e) { $output = new BaseObject(-2, $e->getMessage()); + $output->add('rx_error_location', $e->getUserFileAndLine()); } if ($trigger_name !== 'common.writeSlowlog') diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index 687c07bad..06fdc6b16 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -210,9 +210,10 @@ class ModuleObject extends BaseObject return; } - // Set admin layout - if(preg_match('/^disp[A-Z][a-z0-9\_]+Admin/', $this->act)) + // Special treatment for admin actions + if(preg_match('/^disp(?:Admin[A-Z]|[A-Z][a-z0-9\_]+Admin)/', $this->act)) { + // Set admin layout if(config('view.manager_layout') === 'admin') { $this->setLayoutPath('modules/admin/tpl'); @@ -223,6 +224,16 @@ class ModuleObject extends BaseObject $oTemplate = new Rhymix\Framework\Template('modules/admin/tpl', '_admin_common.html'); $oTemplate->compile(); } + + // Refresh session + if (!isset($_SESSION['RHYMIX']['admin_accessed']) && !headers_sent()) + { + if (!isset($_SESSION['RHYMIX']['last_refresh']) || $_SESSION['RHYMIX']['last_refresh'] < time() - 10) + { + $_SESSION['RHYMIX']['admin_accessed'] = \RX_TIME; + Rhymix\Framework\Session::refresh(true); + } + } } // Execute init @@ -805,6 +816,10 @@ class ModuleObject extends BaseObject { $this->setError($triggerOutput->getError()); $this->setMessage($triggerOutput->getMessage()); + if ($triggerOutput->get('rx_error_location')) + { + $this->add('rx_error_location', $triggerOutput->get('rx_error_location')); + } return FALSE; } @@ -846,6 +861,10 @@ class ModuleObject extends BaseObject { $this->setError($triggerOutput->getError()); $this->setMessage($triggerOutput->getMessage()); + if ($triggerOutput->get('rx_error_location')) + { + $this->add('rx_error_location', $triggerOutput->get('rx_error_location')); + } return false; } diff --git a/common/constants.php b/common/constants.php index ee9fab27d..d4f00f1e9 100644 --- a/common/constants.php +++ b/common/constants.php @@ -3,7 +3,7 @@ /** * RX_VERSION is the version number of the Rhymix CMS. */ -define('RX_VERSION', '2.1.32'); +define('RX_VERSION', '2.1.33'); /** * RX_MICROTIME is the startup time of the current script, in microseconds since the Unix epoch. diff --git a/common/defaults/config.php b/common/defaults/config.php index 6742f7af5..5f149314b 100644 --- a/common/defaults/config.php +++ b/common/defaults/config.php @@ -150,7 +150,6 @@ return array( 'regexp' => '', ], 'use_rewrite' => true, - 'use_sso' => false, 'other' => [ 'proxy' => null, ], diff --git a/common/framework/Exception.php b/common/framework/Exception.php index 6cd07d412..89b1a6739 100644 --- a/common/framework/Exception.php +++ b/common/framework/Exception.php @@ -19,6 +19,11 @@ class Exception extends \Exception public function getUserFileAndLine(): string { $regexp = '!^' . preg_quote(\RX_BASEDIR, '!') . '(?:classes|common)/!'; + if (!preg_match($regexp, $this->getFile())) + { + return $this->getFile() . ':' . $this->getLine(); + } + $trace = $this->getTrace(); foreach ($trace as $frame) { diff --git a/common/framework/Session.php b/common/framework/Session.php index 1a6e7242d..4cf729897 100644 --- a/common/framework/Session.php +++ b/common/framework/Session.php @@ -79,7 +79,7 @@ class Session ini_set('session.use_cookies', 1); ini_set('session.use_only_cookies', 1); ini_set('session.use_strict_mode', 1); - ini_set('session.cookie_samesite', $samesite ? 1 : 0); + ini_set('session.cookie_samesite', $samesite); session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); session_name($session_name = Config::get('session.name') ?: session_name()); @@ -113,7 +113,11 @@ class Session } // Check if the session needs to be refreshed. - if (!$must_create && !isset($_SESSION['RHYMIX']['domains'][$alt_domain]['started']) || $_SESSION['RHYMIX']['domains'][$alt_domain]['started'] < time() - $refresh_interval) + if (!$must_create && (!isset($_SESSION['RHYMIX']['last_refresh']) || $_SESSION['RHYMIX']['last_refresh'] < time() - $refresh_interval)) + { + $must_refresh = true; + } + if (!$must_create && isset($_SESSION['RHYMIX']['next_refresh']) && $_SESSION['RHYMIX']['next_refresh'] === true) { $must_refresh = true; } @@ -127,8 +131,9 @@ class Session } // If this is not a GET request, do not refresh now. - if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'GET') + if ($must_refresh && (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'GET')) { + $_SESSION['RHYMIX']['next_refresh'] = true; $must_refresh = false; } @@ -237,107 +242,6 @@ class Session } } - /** - * Check if this session needs to be shared with another site with SSO. - * - * This method uses more or less the same logic as XE's SSO mechanism. - * It may need to be changed to a more secure mechanism later. - * - * @param object $site_module_info - * @return void - */ - public static function checkSSO(object $site_module_info): void - { - // Abort if SSO is disabled, the visitor is a robot, or this is not a typical GET request. - if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'GET' || !config('use_sso') || UA::isRobot() || in_array(\Context::get('act'), array('rss', 'atom'))) - { - return; - } - - // Get the current site information. - $is_default_domain = ($site_module_info->domain_srl == 0); - if (!$is_default_domain) - { - $current_domain = $site_module_info->domain; - $current_url = URL::getCurrentUrl(); - $default_domain = \ModuleModel::getDefaultDomainInfo(); - $default_url = \Context::getDefaultUrl($default_domain); - } - - // Step 1: if the current site is not the default site, send SSO validation request to the default site. - if(!$is_default_domain && !\Context::get('sso_response') && $_COOKIE['sso'] !== md5($current_domain)) - { - // Set sso cookie to prevent multiple simultaneous SSO validation requests. - Cookie::set('sso', md5($current_domain), array( - 'expires' => 0, - 'path' => '/', - 'domain' => null, - 'secure' => !!config('session.use_ssl'), - 'httponly' => true, - 'samesite' => config('session.samesite'), - )); - - // Redirect to the default site. - $sso_request = Security::encrypt($current_url); - header('Location:' . URL::modifyURL($default_url, array('sso_request' => $sso_request))); - exit; - } - - // Step 2: receive and process SSO validation request at the default site. - if($is_default_domain && \Context::get('sso_request')) - { - // Get the URL of the origin site - $sso_request = Security::decrypt(\Context::get('sso_request')); - if (!$sso_request || !preg_match('!^https?://!', $sso_request)) - { - \Context::displayErrorPage('SSO Error', 'ERR_INVALID_SSO_REQUEST', 400); - exit; - } - if (!URL::isInternalUrl($sso_request) || !URL::isInternalURL($_SERVER['HTTP_REFERER'] ?? '')) - { - \Context::displayErrorPage('SSO Error', 'ERR_INVALID_SSO_REQUEST', 400); - exit; - } - - // Encrypt the session ID. - self::start(true); - $sso_response = Security::encrypt(session_id()); - - // Redirect back to the origin site. - header('Location: ' . URL::modifyURL($sso_request, array('sso_response' => $sso_response))); - self::close(); - exit; - } - - // Step 3: back at the origin site, set session ID to be the same as the default site. - if(!$is_default_domain && \Context::get('sso_response')) - { - // Check SSO response - $sso_response = Security::decrypt(\Context::get('sso_response')); - if ($sso_response === false) - { - \Context::displayErrorPage('SSO Error', 'ERR_INVALID_SSO_RESPONSE', 400); - exit; - } - - // Check that the response was given by the default site (to prevent session fixation CSRF). - if(isset($_SERVER['HTTP_REFERER']) && !URL::isInternalURL($_SERVER['HTTP_REFERER'])) - { - \Context::displayErrorPage('SSO Error', 'ERR_INVALID_SSO_RESPONSE', 400); - exit; - } - - // Set the session ID. - session_id($sso_response); - self::start(true, false); - - // Finally, redirect to the originally requested URL. - header('Location: ' . URL::getCurrentURL(array('sso_response' => null))); - self::close(); - exit; - } - } - /** * Create the data structure for a new Rhymix session. * @@ -351,6 +255,8 @@ class Session $_SESSION['RHYMIX'] = array(); $_SESSION['RHYMIX']['login'] = false; $_SESSION['RHYMIX']['last_login'] = false; + $_SESSION['RHYMIX']['last_refresh'] = time(); + $_SESSION['RHYMIX']['next_refresh'] = false; $_SESSION['RHYMIX']['autologin_key'] = false; $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; @@ -420,7 +326,10 @@ class Session ); // Update the domain initialization timestamp. - $_SESSION['RHYMIX']['domains'][$alt_domain]['started'] = time(); + if (!isset($_SESSION['RHYMIX']['domains'][$alt_domain]['started'])) + { + $_SESSION['RHYMIX']['domains'][$alt_domain]['started'] = time(); + } if (!isset($_SESSION['RHYMIX']['domains'][$alt_domain]['trusted'])) { $_SESSION['RHYMIX']['domains'][$alt_domain]['trusted'] = 0; @@ -429,8 +338,11 @@ class Session // Refresh the main session cookie and the autologin key. if ($refresh_cookie) { + $_SESSION['RHYMIX']['last_refresh'] = time(); + $_SESSION['RHYMIX']['next_refresh'] = false; self::destroyCookiesFromConflictingDomains(array(session_name())); - Cookie::set(session_name(), session_id(), $options); + //Cookie::set(session_name(), session_id(), $options); + session_regenerate_id(true); if (self::$_autologin_key = self::_getAutologinKey()) { self::setAutologinKeys(substr(self::$_autologin_key, 0, 24), substr(self::$_autologin_key, 24, 24)); @@ -534,7 +446,7 @@ class Session if ($refresh) { self::checkLoginStatusCookie(); - return self::refresh(true); + return $_SESSION['RHYMIX']['next_refresh'] = true; } else { @@ -1048,7 +960,7 @@ class Session $path = Config::get('session.path') ?: ini_get('session.cookie_path'); $secure = (\RX_SSL && config('session.use_ssl')) ? true : false; $httponly = Config::get('session.httponly') ?? true; - $samesite = config('session.samesite'); + $samesite = config('session.samesite') ?: ''; return array($lifetime, $refresh, $domain, $path, $secure, $httponly, $samesite); } diff --git a/common/framework/Storage.php b/common/framework/Storage.php index 6637c5cc0..20ea4e04f 100644 --- a/common/framework/Storage.php +++ b/common/framework/Storage.php @@ -873,6 +873,45 @@ class Storage } } + /** + * Prevent access to a directory by creating .htaccess and index.html files in it. + * + * This is a best-effort measure only, and depends on web server configuration. + * It is recommended to use proper server configuration to protect sensitive directories. + * + * @param string $dirname + * @return bool + */ + public static function protectDirectory(string $dirname): bool + { + $dirname = rtrim($dirname, '/\\'); + if (!self::isDirectory($dirname) || !self::isWritable($dirname)) + { + return false; + } + + $result = self::write($dirname . '/index.html', ''); + if (!$result) + { + return false; + } + $result = self::write($dirname . '/.htaccess', preg_replace('/\\t/', '', << + Require all denied + + + Order deny,allow + Deny from all + + END)); + if (!$result) + { + return false; + } + + return true; + } + /** * Get the current umask. * diff --git a/common/framework/filters/FileContentFilter.php b/common/framework/filters/FileContentFilter.php index 6bfa5cf53..cc07924c2 100644 --- a/common/framework/filters/FileContentFilter.php +++ b/common/framework/filters/FileContentFilter.php @@ -51,28 +51,41 @@ class FileContentFilter } // Check other image files. - if (in_array($ext, array('jpg', 'jpeg', 'png', 'gif')) && $mime_type !== false && $mime_type !== 'image') + if (preg_match('/^(jpe?g|png|gif|webp)$/', $ext)) { - fclose($fp); - return false; + if ($mime_type !== false && $mime_type !== 'image') + { + fclose($fp); + return false; + } + if ($mime_type === 'image' && $is_xml && $image_info = @getimagesize($file)) + { + if ($image_info[0] && $image_info[1] && !empty($image_info[2])) + { + $skip_xml = true; + } + } } // Check audio and video files. - if (preg_match('/(wm[va]|mpe?g|avi|flv|mp[1-4]|as[fx]|wav|midi?|moo?v|qt|r[am]{1,2}|m4v)$/', $file) && $mime_type !== false && $mime_type !== 'audio' && $mime_type !== 'video') + if (preg_match('/^(wm[va]|mpe?g|avi|flv|mp[1-4]|as[fx]|wav|midi?|moo?v|qt|r[am]{1,2}|m4v)$/', $ext)) { - fclose($fp); - return false; + if ($mime_type !== false && $mime_type !== 'audio' && $mime_type !== 'video') + { + fclose($fp); + return false; + } } // Check XML files. - if (($ext === 'xml' || $is_xml) && !self::_checkXML($fp, 0, $filesize)) + if (($ext === 'xml' || ($is_xml && !$skip_xml)) && !self::_checkXML($fp, 0, $filesize)) { fclose($fp); return false; } // Check HTML files. - if (($ext === 'html' || $ext === 'shtml' || $ext === 'xhtml' || $ext === 'phtml' || ($is_xml && !$skip_xml)) && !self::_checkHTML($fp, 0, $filesize)) + if ((preg_match('/html$/', $ext) || ($is_xml && !$skip_xml)) && !self::_checkHTML($fp, 0, $filesize)) { fclose($fp); return false; diff --git a/common/framework/parsers/ConfigParser.php b/common/framework/parsers/ConfigParser.php index a12e3b882..2ed1e019b 100644 --- a/common/framework/parsers/ConfigParser.php +++ b/common/framework/parsers/ConfigParser.php @@ -242,7 +242,6 @@ class ConfigParser $config['file']['umask'] = Storage::recommendUmask(); $config['mobile']['enabled'] = ($db_info->use_mobile_view ?? 'N') === 'N' ? false : true; $config['use_rewrite'] = ($db_info->use_rewrite ?? 'N') === 'Y' ? true : false; - $config['use_sso'] = ($db_info->use_sso ?? 'N') === 'Y' ? true : false; // Copy other configuration. unset($db_info->master_db, $db_info->slave_db); diff --git a/modules/addon/addon.admin.model.php b/modules/addon/addon.admin.model.php index 61ba824ea..51f43fe0f 100644 --- a/modules/addon/addon.admin.model.php +++ b/modules/addon/addon.admin.model.php @@ -61,7 +61,7 @@ class addonAdminModel extends addon else { $package = $oAutoinstallModel->getInstalledPackages($packageSrl); - $addonList[$key]->need_update = $package[$packageSrl]->need_update; + $addonList[$key]->need_update = $package[$packageSrl]->need_update ?? null; } // get easyinstall update url diff --git a/modules/addon/tpl/setup_addon.html b/modules/addon/tpl/setup_addon.html index 3d9172e62..21a2ab0f2 100644 --- a/modules/addon/tpl/setup_addon.html +++ b/modules/addon/tpl/setup_addon.html @@ -58,7 +58,7 @@ - +

{$var->group}

{@$group = $var->group}
@@ -103,9 +103,9 @@
- {$modules->title}{$lang->none_category} + {$modules->title}{$lang->none_category} -