diff --git a/mokosuitebackup.php b/mokosuitebackup.php deleted file mode 100644 index 2954662..0000000 --- a/mokosuitebackup.php +++ /dev/null @@ -1,11 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - */ - -defined('_JEXEC') or die; diff --git a/mokosuitebackup.xml b/mokosuitebackup.xml deleted file mode 100644 index 962b0f7..0000000 --- a/mokosuitebackup.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - Web Services - MokoSuiteBackup - 01.27.03 - 2026-06-02 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION - - Joomla\Plugin\WebServices\MokoSuiteBackup - - - mokosuitebackup.php - services - src - - - - language/en-GB/plg_webservices_mokosuitebackup.ini - language/en-GB/plg_webservices_mokosuitebackup.sys.ini - - diff --git a/source/packages/com_mokosuitebackup/forms/filter_backups.xml b/source/packages/com_mokosuitebackup/forms/filter_backups.xml index 11af4cc..a44abd5 100644 --- a/source/packages/com_mokosuitebackup/forms/filter_backups.xml +++ b/source/packages/com_mokosuitebackup/forms/filter_backups.xml @@ -19,6 +19,18 @@ + + + + + + + diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index 72366dd..bf9735b 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -167,6 +167,7 @@ COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending" COM_MOKOJOOMBACKUP_FILTER_SEARCH="Search" COM_MOKOJOOMBACKUP_FILTER_STATUS="Status" COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL="- Select Status -" +COM_MOKOJOOMBACKUP_FILTER_TYPE_ALL="- Select Type -" ; Tabs and fieldsets COM_MOKOJOOMBACKUP_TAB_GENERAL="General" diff --git a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php index 4c2f63f..06254ed 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php +++ b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php @@ -303,6 +303,20 @@ function actionExtract(array $data): array $zip->setPassword($password); } + // Validate all entries before extraction (path traversal protection) + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + + if ($entryName === false) { + continue; + } + + if (str_contains($entryName, '../') || str_contains($entryName, '..\\') || str_starts_with($entryName, '/') || str_starts_with($entryName, '\\')) { + $zip->close(); + throw new RuntimeException('Archive contains unsafe path: ' . $entryName); + } + } + if (!$zip->extractTo(RESTORE_DIR)) { $zip->close(); throw new RuntimeException( diff --git a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php index c95ae67..7ffd2b6 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php @@ -191,6 +191,20 @@ class RestoreEngine $this->log('Decryption password set'); } + // Validate all entries before extraction (path traversal protection) + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + + if ($entryName === false) { + continue; + } + + if (str_contains($entryName, '../') || str_contains($entryName, '..\\') || str_starts_with($entryName, '/') || str_starts_with($entryName, '\\')) { + $zip->close(); + throw new \RuntimeException('Archive contains unsafe path: ' . $entryName); + } + } + if (!$zip->extractTo($this->stagingDir)) { $zip->close(); @@ -210,6 +224,18 @@ class RestoreEngine private function extractTarGz(string $archivePath): void { $phar = new \PharData($archivePath); + + // Validate all entries before extraction (path traversal protection) + foreach (new \RecursiveIteratorIterator($phar) as $entry) { + $entryName = $entry->getPathname(); + // PharData paths are prefixed with phar:// — extract the relative part + $relative = substr($entryName, strlen('phar://' . $archivePath) + 1); + + if (str_contains($relative, '../') || str_contains($relative, '..\\') || str_starts_with($relative, '/') || str_starts_with($relative, '\\')) { + throw new \RuntimeException('Archive contains unsafe path: ' . $relative); + } + } + $phar->extractTo($this->stagingDir, null, true); $this->log('Extracted tar.gz archive'); } diff --git a/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php b/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php index c7466d3..d3290ad 100644 --- a/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php +++ b/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php @@ -61,6 +61,13 @@ class BackupsModel extends ListModel $query->where($db->quoteName('a.profile_id') . ' = ' . (int) $profileId); } + // Filter by backup type + $backupType = $this->getState('filter.backup_type'); + + if (!empty($backupType)) { + $query->where($db->quoteName('a.backup_type') . ' = ' . $db->quote($backupType)); + } + // Filter by search $search = $this->getState('filter.search');