fix: Remove orphaned root-level webservices plugin files #87

Merged
jmiller merged 2 commits from fix/remove-orphaned-root-files into main 2026-06-21 23:56:04 +00:00
7 changed files with 60 additions and 42 deletions
-11
View File
@@ -1,11 +0,0 @@
<?php
/**
* @package MokoSuiteBackup
* @subpackage plg_webservices_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* @package MokoSuiteBackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.27.03</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\WebServices\MokoSuiteBackup</namespace>
<files>
<filename plugin="mokosuitebackup">mokosuitebackup.php</filename>
<folder>services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_webservices_mokosuitebackup.ini</language>
<language tag="en-GB">language/en-GB/plg_webservices_mokosuitebackup.sys.ini</language>
</languages>
</extension>
@@ -19,6 +19,18 @@
<option value="fail">COM_MOKOJOOMBACKUP_STATUS_FAIL</option>
<option value="pending">COM_MOKOJOOMBACKUP_STATUS_PENDING</option>
</field>
<field
name="backup_type"
type="list"
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE"
onchange="this.form.submit();"
>
<option value="">COM_MOKOJOOMBACKUP_FILTER_TYPE_ALL</option>
<option value="full">COM_MOKOJOOMBACKUP_TYPE_FULL</option>
<option value="database">COM_MOKOJOOMBACKUP_TYPE_DATABASE</option>
<option value="files">COM_MOKOJOOMBACKUP_TYPE_FILES</option>
<option value="differential">COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL</option>
</field>
</fields>
<fields name="list">
@@ -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"
@@ -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(
@@ -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');
}
@@ -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');