From b048b47e7c5406847b880bcbd2217a1a0cabc4fa Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 24 May 2026 03:53:33 -0500 Subject: [PATCH] security: protected status prevents disable/uninstall - Set protected=1, locked=0 on MokoWaaS extensions via package script - Self-healing: plugin checks and restores protected flag each session - Block non-master disable via plugin list toggle (plugins.publish) - Block non-master uninstall via installer manage - Joomla framework natively enforces protected status (greys out toggles) - Master users can still manage settings and updates Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 54 ++++++++++++++++++- src/script.php | 33 ++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 86ec2a2..62e935b 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -985,6 +985,15 @@ class MokoWaaS extends CMSPlugin */ protected function protectPlugin() { + // Ensure protected flag is set (self-healing — runs once per session) + static $flagChecked = false; + + if (!$flagChecked) + { + $flagChecked = true; + $this->ensureProtectedFlag(); + } + if ($this->isMasterUser()) { return; @@ -993,7 +1002,7 @@ class MokoWaaS extends CMSPlugin $option = $this->app->input->get('option', ''); $task = $this->app->input->get('task', ''); - // Block non-master from disabling or uninstalling MokoWaaS + // Block non-master from uninstalling MokoWaaS if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false) { $cid = $this->app->input->get('cid', [], 'array'); @@ -1004,6 +1013,49 @@ class MokoWaaS extends CMSPlugin $this->app->redirect('index.php?option=com_installer&view=manage'); } } + + // Block non-master from disabling via list toggle + if ($option === 'com_plugins' && strpos($task, 'plugins.publish') !== false) + { + $cid = $this->app->input->get('cid', [], 'array'); + + if ($this->isOurExtension($cid)) + { + $this->app->enqueueMessage('MokoWaaS cannot be disabled.', 'error'); + $this->app->redirect('index.php?option=com_plugins'); + } + } + } + + /** + * Ensure the protected flag is set on MokoWaaS extensions in the DB. + * + * Sets protected=1, locked=0 so the extension can't be disabled or + * uninstalled but can still receive updates and config changes. + * + * @return void + * + * @since 02.03.10 + */ + protected function ensureProtectedFlag() + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('protected') . ' = 1') + ->set($db->quoteName('locked') . ' = 0') + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')') + ->where($db->quoteName('protected') . ' = 0'); + $db->setQuery($query); + $db->execute(); + } + catch (\Throwable $e) + { + // Non-critical + } } /** diff --git a/src/script.php b/src/script.php index e7ea151..cc58abf 100644 --- a/src/script.php +++ b/src/script.php @@ -37,6 +37,9 @@ class Pkg_MokowaasInstallerScript $this->enablePlugin('system', 'mokowaas'); $this->enablePlugin('webservices', 'mokowaas'); + // Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level) + $this->protectExtensions(); + // Trigger heartbeat registration $this->sendHeartbeat(); } @@ -71,6 +74,36 @@ class Pkg_MokowaasInstallerScript } } + /** + * Set the protected flag on all MokoWaaS extensions. + * + * Joomla's protected flag prevents disabling and uninstalling at the + * framework level — no plugin-side interception needed. + * + * @return void + * + * @since 02.03.10 + */ + private function protectExtensions(): void + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('protected') . ' = 1') + ->set($db->quoteName('locked') . ' = 0') + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')'); + $db->setQuery($query); + $db->execute(); + } + catch (\Throwable $e) + { + Log::add('Error protecting MokoWaaS extensions: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + } + /** * Send heartbeat to the MokoWaaS monitoring receiver. *