fix: harden input handling and output safety (#79)

- canonical_url: sanitize via sanitizeUrl() (scheme allowlist) instead of
  bare trim() — closes stored-XSS via addHeadLink() on the public frontend
- AI endpoint: replace die('Invalid Token') with a clean event result,
  and strip_tags + truncate article_title to 200 chars before use
- SitemapBuilder: whitelist changefreq against the sitemap spec enum,
  intval() noindex IDs, strict in_array comparison
- MokoOG: log a WARNING when sitemap.xml write fails instead of ignoring it

(cherry picked from commit b77054b769)
This commit is contained in:
2026-06-28 13:54:53 -05:00
parent dbf726e148
commit d9087ac420
3 changed files with 15 additions and 6 deletions
@@ -274,7 +274,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
'seo_title' => strip_tags(trim($ogData['seo_title'] ?? '')),
'meta_description' => strip_tags(trim($ogData['meta_description'] ?? '')),
'robots' => trim($robots),
'canonical_url' => trim($ogData['canonical_url'] ?? ''),
'canonical_url' => $this->sanitizeUrl($ogData['canonical_url'] ?? ''),
'published' => 1,
'modified' => Factory::getDate()->toSql(),
];
@@ -832,7 +832,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$changefreq = $this->params->get('sitemap_changefreq', 'weekly');
$xml = SitemapBuilder::generate($changefreq);
SitemapBuilder::writeToFile($xml);
if (!SitemapBuilder::writeToFile($xml)) {
\Joomla\CMS\Log\Log::add('MokoOG: Failed to write sitemap.xml — check file permissions', \Joomla\CMS\Log\Log::WARNING, 'mokoog');
}
}
/**
@@ -850,7 +853,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return;
}
\Joomla\CMS\Session\Session::checkToken() or die('Invalid Token');
if (!\Joomla\CMS\Session\Session::checkToken()) {
$event->setArgument('result', ['Invalid Token']);
return;
}
if (!$this->params->get('ai_enabled', 0)) {
$event->setArgument('result', ['AI generation is not enabled']);
@@ -868,7 +874,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$input = $app->getInput();
$field = $input->getString('field', 'title');
$articleTitle = $input->getString('article_title', '');
$articleTitle = mb_substr(strip_tags($input->getString('article_title', '')), 0, 200);
$prompt = $field === 'title'
? "Generate a concise, engaging social media sharing title (max 60 characters) for an article titled: \"$articleTitle\". Return only the title text, no quotes or explanation."
@@ -32,6 +32,9 @@ class SitemapBuilder
*/
public static function generate(string $changefreq = 'weekly'): string
{
$allowed = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
$changefreq = \in_array($changefreq, $allowed, true) ? $changefreq : 'weekly';
$db = Factory::getDbo();
// Get all published articles
@@ -51,7 +54,7 @@ class SitemapBuilder
->where($db->quoteName('robots') . ' LIKE ' . $db->quote('%noindex%'));
$db->setQuery($noindexQuery);
$noindexIds = $db->loadColumn();
$noindexIds = array_map('intval', $db->loadColumn());
$root = rtrim(Uri::root(), '/');
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
@@ -66,7 +69,7 @@ class SitemapBuilder
foreach ($articles as $article) {
// Skip noindexed
if (in_array((int) $article->id, $noindexIds)) {
if (\in_array((int) $article->id, $noindexIds, true)) {
continue;
}