fix: code review — 10 security and quality fixes
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
#1 exportSettings: added CSRF token check (Session::checkToken('get')) #2 All controllers: added explicit return after jsonForbidden() and early-exit jsonResponse() calls — prevents execution fallthrough #3 Delete/update handlers: return validation implicit via getInt #6 WAF scanInput: double urldecode to catch %25xx encoding tricks #7 PrivacyModel anonymize: now clears #__user_profiles and #__contact_details (GDPR completeness) #12 SLA responded: only marked on staff replies, not customer self-replies — prevents customers from clearing their own SLA timer #13 MokoWaaSHelper: getMasterUsernames() changed to private static — prevents third-party code from accessing decoded master usernames Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.plugins.toggle'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$app = Factory::getApplication();
|
||||
@@ -87,6 +88,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.cache'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse($this->getModel('Dashboard')->clearCache());
|
||||
@@ -103,6 +105,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.extensions'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$downloadUrl = Factory::getApplication()->getInput()->getString('download_url', '');
|
||||
@@ -110,6 +113,7 @@ class DisplayController extends BaseController
|
||||
if (empty($downloadUrl))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl));
|
||||
@@ -126,6 +130,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.htaccess'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$app = Factory::getApplication();
|
||||
@@ -157,6 +162,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.htaccess'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$model = $this->getModel('Htaccess');
|
||||
@@ -184,6 +190,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.tickets.create'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -203,6 +210,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -221,6 +229,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -385,9 +394,12 @@ class DisplayController extends BaseController
|
||||
|
||||
public function exportSettings()
|
||||
{
|
||||
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
@@ -431,6 +443,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$json = Factory::getApplication()->getInput()->getRaw('settings_json', '');
|
||||
@@ -439,6 +452,7 @@ class DisplayController extends BaseController
|
||||
if (empty($data) || empty($data['plugins']))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
@@ -488,6 +502,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$days = Factory::getApplication()->getInput()->getInt('days', 30);
|
||||
@@ -503,6 +518,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = Factory::getApplication()->getInput()->getString('ip', '');
|
||||
@@ -522,6 +538,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -540,6 +557,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
@@ -560,6 +578,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse($this->getModel('Import')->importAts());
|
||||
@@ -572,6 +591,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse($this->getModel('Import')->importAdminTools());
|
||||
@@ -614,5 +634,6 @@ class DisplayController extends BaseController
|
||||
private function jsonForbidden(): void
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,28 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
// Clear Joomla user profile fields (#7)
|
||||
try
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
// Clear contact details if linked
|
||||
try
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__contact_details'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
// Log the anonymization
|
||||
$this->logConsent($userId, 'account_anonymized', 'granted');
|
||||
|
||||
|
||||
@@ -207,15 +207,22 @@ class TicketsModel extends BaseDatabaseModel
|
||||
|
||||
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
|
||||
|
||||
// Mark SLA as responded if first staff reply
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->set($db->quoteName('sla_responded') . ' = 1')
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
->where($db->quoteName('sla_responded') . ' = 0')
|
||||
)->execute();
|
||||
// Mark SLA as responded only for staff replies (not customer self-replies)
|
||||
$ticket = $this->getTicket($ticketId);
|
||||
$isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by;
|
||||
|
||||
$updateQuery = $db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId);
|
||||
|
||||
if ($isStaffReply)
|
||||
{
|
||||
$updateQuery->set($db->quoteName('sla_responded') . ' = 1')
|
||||
->where($db->quoteName('sla_responded') . ' = 0');
|
||||
}
|
||||
|
||||
$db->setQuery($updateQuery)->execute();
|
||||
|
||||
// Run automation + notifications (skip internal notes)
|
||||
$this->runAutomation('ticket_replied', $ticketId);
|
||||
|
||||
@@ -50,6 +50,7 @@ class DisplayController extends BaseController
|
||||
if ($user->guest)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -78,6 +79,7 @@ class DisplayController extends BaseController
|
||||
if ($user->guest)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$ticketId = $input->getInt('ticket_id', 0);
|
||||
@@ -87,12 +89,14 @@ class DisplayController extends BaseController
|
||||
if (!$ticket)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Ticket not found.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Customers can only reply to their own tickets; staff can reply to any
|
||||
if ((int) $ticket->created_by !== $user->id && !$this->isStaff($user))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Staff replies from frontend are not internal notes
|
||||
@@ -115,6 +119,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->isStaff($user))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -138,6 +143,7 @@ class DisplayController extends BaseController
|
||||
if (!$user->authorise('mokowaas.tickets.assign', 'com_mokowaas'))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
@@ -160,6 +166,7 @@ class DisplayController extends BaseController
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +182,7 @@ class DisplayController extends BaseController
|
||||
if ($user->guest)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$type = Factory::getApplication()->getInput()->getString('type', '');
|
||||
|
||||
@@ -52,7 +52,7 @@ final class MokoWaaSHelper
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getMasterUsernames(): array
|
||||
private static function getMasterUsernames(): array
|
||||
{
|
||||
if (self::$masterNames !== null)
|
||||
{
|
||||
|
||||
@@ -503,7 +503,8 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
$decoded = urldecode($value);
|
||||
// Double-decode to catch %25xx encoding tricks
|
||||
$decoded = urldecode(urldecode($value));
|
||||
|
||||
if (preg_match($pattern, $value) || preg_match($pattern, $decoded))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user