fix(security+reliability): address PR review — ACL guards, error logging, path traversal
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
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 / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
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
Security: - Add return after all jsonForbidden() calls (13 methods) to prevent ACL bypass if $app->close() fails to terminate - Add throw after requireAuth() in REST API controller (same pattern) - Add path traversal guard to AttachmentService::getAbsolutePath() using realpath + prefix check Error handling: - Log install notification email failures instead of empty catch - Log DB errors in getUserEmail(), getNotificationConfig(), getComponentConfig() instead of silent fallbacks - Log PHP upload error codes in AttachmentService - Check Folder::create() return value before upload loop - Fix searchKb() missing return on short query + log DB errors - Fix ntfy push to capture curl_error() on connection failure - Upgrade AutomationEngine inner catch to LOG_ERROR with rule ID
This commit is contained in:
@@ -500,6 +500,7 @@ class DisplayController extends BaseController
|
||||
if (strlen($query) < 3)
|
||||
{
|
||||
$this->jsonResponse(['results' => []]);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -527,7 +528,8 @@ class DisplayController extends BaseController
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->jsonResponse(['results' => []]);
|
||||
Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
|
||||
$this->jsonResponse(['results' => [], 'error' => 'Search unavailable']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +577,7 @@ class DisplayController extends BaseController
|
||||
public function saveCategory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$id = $input->getInt('id', 0);
|
||||
@@ -600,7 +602,7 @@ class DisplayController extends BaseController
|
||||
public function deleteCategory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
|
||||
@@ -609,7 +611,7 @@ class DisplayController extends BaseController
|
||||
public function reorderCategory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
||||
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
||||
$db = Factory::getDbo();
|
||||
@@ -622,7 +624,7 @@ class DisplayController extends BaseController
|
||||
public function saveCanned()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$data = (object) [
|
||||
@@ -640,7 +642,7 @@ class DisplayController extends BaseController
|
||||
public function deleteCanned()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
|
||||
@@ -649,7 +651,7 @@ class DisplayController extends BaseController
|
||||
public function reorderCanned()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
||||
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
||||
$db = Factory::getDbo();
|
||||
@@ -662,7 +664,7 @@ class DisplayController extends BaseController
|
||||
public function uploadAttachment()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$ticketId = $input->getInt('ticket_id', 0);
|
||||
$replyId = $input->getInt('reply_id', 0) ?: null;
|
||||
@@ -675,7 +677,7 @@ class DisplayController extends BaseController
|
||||
|
||||
public function downloadAttachment()
|
||||
{
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_ticket_attachments')->where('id = ' . $id));
|
||||
@@ -696,7 +698,7 @@ class DisplayController extends BaseController
|
||||
public function deleteAttachment()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$ok = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::delete($id);
|
||||
$this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']);
|
||||
@@ -705,7 +707,7 @@ class DisplayController extends BaseController
|
||||
public function rateTicket()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$ticketId = $input->getInt('ticket_id', 0);
|
||||
$rating = $input->getInt('rating', 0);
|
||||
@@ -728,7 +730,7 @@ class DisplayController extends BaseController
|
||||
public function saveAutomation()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$data = (object) [
|
||||
@@ -749,7 +751,7 @@ class DisplayController extends BaseController
|
||||
public function deleteAutomation()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
|
||||
@@ -758,7 +760,7 @@ class DisplayController extends BaseController
|
||||
public function toggleAutomation()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->update('#__mokosuiteclient_ticket_automation')
|
||||
@@ -770,7 +772,7 @@ class DisplayController extends BaseController
|
||||
public function reorderAutomation()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
||||
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
||||
$db = Factory::getDbo();
|
||||
|
||||
@@ -52,8 +52,9 @@ class AttachmentService
|
||||
|
||||
$ticketDir = self::STORAGE_DIR . '/' . $ticketId;
|
||||
|
||||
if (!is_dir($ticketDir)) {
|
||||
Folder::create($ticketDir);
|
||||
if (!is_dir($ticketDir) && !Folder::create($ticketDir)) {
|
||||
Log::add("Failed to create attachment directory: {$ticketDir}", Log::ERROR, 'mokosuiteclient');
|
||||
return [];
|
||||
}
|
||||
|
||||
$userId = (int) Factory::getUser()->id;
|
||||
@@ -62,6 +63,7 @@ class AttachmentService
|
||||
for ($i = 0, $count = count($files['name']); $i < $count; $i++)
|
||||
{
|
||||
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
|
||||
Log::add("Attachment upload error for '{$files['name'][$i]}': PHP error code {$files['error'][$i]}", Log::WARNING, 'mokosuiteclient');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -127,9 +129,13 @@ class AttachmentService
|
||||
/**
|
||||
* Get the absolute filesystem path for an attachment.
|
||||
*/
|
||||
public static function getAbsolutePath(object $attachment): string
|
||||
public static function getAbsolutePath(object $attachment): ?string
|
||||
{
|
||||
return self::STORAGE_DIR . '/' . $attachment->filepath;
|
||||
$path = realpath(self::STORAGE_DIR . '/' . $attachment->filepath);
|
||||
if ($path === false || !str_starts_with($path, realpath(self::STORAGE_DIR))) {
|
||||
return null;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -187,7 +187,7 @@ class AutomationEngine
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add("Automation action {$type} failed: " . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
||||
Log::add("Automation action '{$type}' failed for rule #{$rule->id}: " . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,7 @@ class NotificationService
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Failed to look up email for user ID ' . $userId . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -331,6 +332,7 @@ class NotificationService
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Failed to load notification config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -397,12 +399,16 @@ class NotificationService
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_exec($ch);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300)
|
||||
if ($response === false)
|
||||
{
|
||||
Log::add("Ntfy push connection failed for event {$event}: " . $curlError, Log::WARNING, 'mokosuiteclient');
|
||||
}
|
||||
elseif ($httpCode < 200 || $httpCode >= 300)
|
||||
{
|
||||
Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuiteclient');
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ class TicketsController extends BaseController
|
||||
$user = Factory::getUser();
|
||||
if (!$user->authorise($action, $asset)) {
|
||||
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||
throw new \RuntimeException('Not authorized', 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ class plgSystemMokoSuiteClientInstallerScript implements InstallerScriptInterfac
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
// Don't break install if email fails
|
||||
Log::add('Install notification email failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -251,6 +251,7 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Failed to load component config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user