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 @@
-
+
last_page">{$lang->last_page} »
diff --git a/modules/page/page.admin.controller.php b/modules/page/page.admin.controller.php
index 53066d5ca..12966a0ec 100644
--- a/modules/page/page.admin.controller.php
+++ b/modules/page/page.admin.controller.php
@@ -23,16 +23,16 @@ class PageAdminController extends Page
$args = Context::getRequestVars();
$args->module = 'page';
$args->mid = $args->page_name; //because if mid is empty in context, set start page mid
- $args->path = (!$args->path) ? '' : $args->path;
- $args->mpath = (!$args->mpath) ? '' : $args->mpath;
- if (preg_match('!\bfiles/cache/!i', $args->path))
+ $args->path = isset($args->path) ? strval($args->path) : '';
+ $args->mpath = isset($args->mpath) ? strval($args->mpath) : '';
+ if (!self::_isAllowedExternalPath($args->path))
{
$this->setError(-1);
$this->setMessage('msg_invalid_opage_pc_path');
$this->setRedirectUrl(Context::get('success_return_url'));
return;
}
- if (preg_match('!\bfiles/cache/!i', $args->mpath))
+ if (!self::_isAllowedExternalPath($args->mpath))
{
$this->setError(-1);
$this->setMessage('msg_invalid_opage_mobile_path');
@@ -383,6 +383,42 @@ class PageAdminController extends Page
// 성공 메세지 등록
$this->setMessage($msg_code);
}
+
+ /**
+ * Check if the path to an external page is valid.
+ *
+ * @param string $path
+ * @return bool
+ */
+ protected static function _isAllowedExternalPath(string $path): bool
+ {
+ // Normalize the directory separator.
+ $path = str_replace('\\', '/', $path);
+
+ // Check for forbidden paths.
+ if (preg_match('!(?:^|/)files/(?:attach|cache|config|debug|env|member_extra_info|ruleset|site_design|thumbnails)/!i', $path))
+ {
+ return false;
+ }
+
+ // Check for forbidden extensions.
+ if (preg_match('!(?:image|audio|video)/!i', Rhymix\Framework\MIME::getTypeByFilename($path)))
+ {
+ return false;
+ }
+
+ // Run the check again after resolving any symbolic links.
+ if (!preg_match('!^https?://!i', $path) && file_exists($path))
+ {
+ $realpath = realpath($path);
+ if ($realpath !== $path && !self::_isAllowedExternalPath($realpath))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
/* End of file page.admin.controller.php */
/* Location: ./modules/page/page.admin.controller.php */
diff --git a/modules/widget/tpl/downloaded_widget_list.html b/modules/widget/tpl/downloaded_widget_list.html
index 047258383..3de6c18dc 100644
--- a/modules/widget/tpl/downloaded_widget_list.html
+++ b/modules/widget/tpl/downloaded_widget_list.html
@@ -29,7 +29,7 @@
- isBlacklisted">{$widget->version}
+ isBlacklisted)">{$widget->version}
diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php
index e0675dfc6..af8a6dcd8 100644
--- a/tests/unit/framework/SessionTest.php
+++ b/tests/unit/framework/SessionTest.php
@@ -98,11 +98,6 @@ class SessionTest extends \Codeception\Test\Unit
$this->assertTrue(Rhymix\Framework\Session::isStarted());
}
- public function testCheckSSO()
- {
- $this->assertNull(Rhymix\Framework\Session::checkSSO(new stdClass));
- }
-
public function testRefresh()
{
$_SERVER['REQUEST_METHOD'] = 'GET';
diff --git a/tests/unit/framework/StorageTest.php b/tests/unit/framework/StorageTest.php
index 4b17ead0c..7c9e79bc2 100644
--- a/tests/unit/framework/StorageTest.php
+++ b/tests/unit/framework/StorageTest.php
@@ -346,6 +346,16 @@ class StorageTest extends \Codeception\Test\Unit
$this->assertFalse(Rhymix\Framework\Storage::deleteDirectory($nonexistent));
}
+ public function testProtectDirectory()
+ {
+ $dir = \RX_BASEDIR . 'tests/_output/protectdir';
+ mkdir($dir);
+ $this->assertTrue(Rhymix\Framework\Storage::protectDirectory($dir));
+ $this->assertTrue(file_exists($dir . '/index.html'));
+ $this->assertTrue(file_exists($dir . '/.htaccess'));
+ $this->assertStringContainsString('Require all denied', file_get_contents($dir . '/.htaccess'));
+ }
+
public function testDeleteDirectoryKeepRoot()
{
$sourcedir = \RX_BASEDIR . 'tests/_output/sourcedir';
|