From 4311ec8f99b2d75c1fd2366e61e2c9b8943aec06 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 5 Jul 2026 22:57:40 -0500 Subject: [PATCH] fix(security): enforce scheme allow-list on runbackup return URL The pre-update return URL is assigned to window.location.href, so a javascript:/data: URI (empty host) would have passed the host-only check and executed. Require an absolute http(s) same-host URL or a single-slash root-relative path; reject javascript:/data:/vbscript: and protocol-relative //host. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i --- .../tmpl/runbackup/default.php | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php index d4cceddd..dab4defb 100644 --- a/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/runbackup/default.php @@ -22,23 +22,36 @@ use Joomla\CMS\Uri\Uri; $ajaxToken = Session::getFormToken(); $ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false); -/* Validate the return URL to prevent an open redirect: accept only a relative - URL or one whose host matches this site. Falls back to '' (no redirect). */ +/* Validate the return URL to prevent an open redirect / javascript: XSS + (the value ends up in window.location.href). Accept ONLY: + - an absolute http(s) URL whose host matches this site, or + - a root-relative path starting with a single "/" (not "//"). + This rejects javascript:, data:, vbscript: (empty host) and + protocol-relative //evil.com. Falls back to '' (no redirect). */ $safeReturnUrl = ''; if ($this->returnUrl !== '') { $decoded = base64_decode($this->returnUrl, true); $raw = ($decoded !== false && $decoded !== '') ? $decoded : $this->returnUrl; - try { - $rootHost = Uri::getInstance(Uri::root())->getHost(); - $target = new Uri($raw); + $isAbsoluteHttp = (bool) preg_match('#^https?://#i', $raw); + $isRootRelative = isset($raw[0]) && $raw[0] === '/' && (!isset($raw[1]) || $raw[1] !== '/'); - if ($target->getHost() === '' || strcasecmp($target->getHost(), $rootHost) === 0) { - $safeReturnUrl = $raw; + if ($isAbsoluteHttp || $isRootRelative) { + try { + $rootHost = Uri::getInstance(Uri::root())->getHost(); + $target = new Uri($raw); + $scheme = strtolower((string) $target->getScheme()); + + $schemeOk = $scheme === '' || $scheme === 'http' || $scheme === 'https'; + $hostOk = $target->getHost() === '' || strcasecmp($target->getHost(), $rootHost) === 0; + + if ($schemeOk && $hostOk) { + $safeReturnUrl = $raw; + } + } catch (\Throwable $e) { + $safeReturnUrl = ''; } - } catch (\Throwable $e) { - $safeReturnUrl = ''; } }