VYPR
Moderate severityNVD Advisory· Published Sep 18, 2024· Updated Sep 19, 2024

XSS in contact tracking and page hits report

CVE-2021-27917

Description

Prior to this patch, a stored XSS vulnerability existed in the contact tracking and page hits report.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A stored cross-site scripting (XSS) vulnerability in Mautic's contact tracking and page hits report allows attackers to inject malicious scripts via user-controlled data.

Vulnerability

Overview

Prior to a specific patch, Mautic, an open-source marketing automation platform, contained a stored cross-site scripting (XSS) vulnerability in its contact tracking and page hits report [1][3]. The root cause involved unsafe handling of user-controlled input that was later rendered in the page title and modal dialogs. In the JavaScript code, the generatePageTitle function constructed the page title by concatenating user names or other dynamic data directly into a jQuery HTML string, which allowed encoded HTML entities to be decoded and executed [2][4]. Similarly, the loadAjaxModal function used .html() to set modal headers, bypassing proper output encoding [2][4].

Exploitation

An attacker could exploit this vulnerability by crafting a malicious payload (e.g., via a contact's company name or user name) that includes HTML or JavaScript code [4]. When the vulnerable report renders that data in the page title or modal dialogs, the injected script executes in the context of the victim's browser session [2][4]. No authentication or special privileges are required to trigger the stored XSS if the attacker can insert data into the system, as the vulnerability lies in how the application processes and displays such data. The attack vector is through the contact tracking endpoint (e.g., /mtc/event), where parameters like page_url containing encoded payloads are stored and later displayed [4].

Impact

Successful exploitation could allow an attacker to execute arbitrary JavaScript in the browsers of users who view the affected reports or dialogs [2][4]. This could lead to session hijacking, defacement, or redirection to malicious sites. Since the vulnerability is stored, any administrator or user accessing the dashboard or page hits report would be affected, potentially allowing lateral movement or data theft within the Mautic installation.

Mitigation

The vulnerability was fixed in a subsequent patch that sanitizes output by using .text() instead of .html() for dynamic content in page titles and modal headers, and by escaping output with $view->escape() in template files [2][4]. Users are strongly advised to update to the latest patched version of Mautic (4.4.x or later) to remediate this issue [1][2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mautic/core-libPackagist
>= 1.0.0-beta4, < 4.4.134.4.13
mautic/core-libPackagist
>= 5.0.0-alpha, < 5.1.15.1.1
mautic/corePackagist
>= 1.0.0-beta4, < 4.4.134.4.13
mautic/corePackagist
>= 5.0.0-alpha, < 5.1.15.1.1

Affected products

3

Patches

2
550e33562d03

Merge remote-tracking branch 'security/DPMMA-2855_mtc-contact-xss-v5' into 5.1

https://github.com/mautic/mauticJohn LinhartSep 18, 2024via ghsa
4 files changed · +48 7
  • app/bundles/CoreBundle/Assets/js/1a.content.js+2 2 modified
    @@ -204,8 +204,8 @@ Mautic.generatePageTitle = function(route){
                 currentModuleItem = mQuery('.page-header h3').text();
             }
     
    -        // Encoded entites are decoded by this process and can cause a XSS
    -        currentModuleItem = mQuery('<div>'+currentModuleItem+'</div>').text();
    +        // Safely set the text content to prevent XSS
    +        currentModuleItem = mQuery('<div>').text(currentModuleItem).html();
     
             mQuery('title').html( currentModule[0].toUpperCase() + currentModule.slice(1) + ' | ' + currentModuleItem + ' | Mautic' );
         } else {
    
  • app/bundles/CoreBundle/Assets/js/9.modals.js+2 1 modified
    @@ -90,7 +90,8 @@ Mautic.loadAjaxModal = function (target, route, method, header, footer, preventD
         //move the modal to the body tag to get around positioned div issues
         element.one('show.bs.modal', function () {
             if (header) {
    -            element.find(".modal-title").html(header);
    +            // use text instead of html method to prevent XSS
    +            element.find(".modal-title").text(header);
             }
     
             if (footer && footer != 'false') {
    
  • app/bundles/LeadBundle/Resources/views/Auditlog/details.html.twig+0 4 modified
    @@ -24,7 +24,3 @@
             {{ 'mautic.lead.audit.merged'|trans }}
         {% endif %}
     {% endif %}
    -<!--
    -Event Type: "{{ event.eventType }}"
    -{{ event.details|json_encode(constant('JSON_PRETTY_PRINT'))|raw }}
    --->
    \ No newline at end of file
    
  • app/bundles/PageBundle/Tests/Controller/PublicControllerFunctionalTest.php+44 0 added
    @@ -0,0 +1,44 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\PageBundle\Tests\Controller;
    +
    +use Mautic\CoreBundle\Test\MauticMysqlTestCase;
    +use PHPUnit\Framework\Assert;
    +
    +class PublicControllerFunctionalTest extends MauticMysqlTestCase
    +{
    +    public function testMtcEventCompanyXss(): void
    +    {
    +        $this->client->request('POST', '/mtc/event', [
    +            'page_url' => 'https://example.com?Company=%3Cimg+src+onerror%3Dalert%28%27Company%27%29%3E',
    +        ]);
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +
    +        $response = json_decode($clientResponse->getContent(), true);
    +
    +        $this->client->request('GET', sprintf('/s/contacts/view/%d', $response['id']));
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +
    +        $crawler        = $this->client->request('GET', sprintf('/s/contacts/edit/%d', $response['id']));
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +
    +        $buttonCrawlerNode = $crawler->selectButton('Save & Close');
    +        $form              = $buttonCrawlerNode->form();
    +        $this->client->submit($form);
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +    }
    +}
    
629165ac905c

Merge branch 'DPMMA-2855_mtc-contact-xss-v4' into 4.4

https://github.com/mautic/mauticJohn LinhartSep 18, 2024via ghsa
7 files changed · +57 13
  • app/bundles/CoreBundle/Assets/js/1a.content.js+2 2 modified
    @@ -203,8 +203,8 @@ Mautic.generatePageTitle = function(route){
                 currentModuleItem = mQuery('.page-header h3').text();
             }
     
    -        // Encoded entites are decoded by this process and can cause a XSS
    -        currentModuleItem = mQuery('<div>'+currentModuleItem+'</div>').text();
    +        // Safely set the text content to prevent XSS
    +        currentModuleItem = mQuery('<div>').text(currentModuleItem).html();
     
             mQuery('title').html( currentModule[0].toUpperCase() + currentModule.slice(1) + ' | ' + currentModuleItem + ' | Mautic' );
         } else {
    
  • app/bundles/CoreBundle/Assets/js/9.modals.js+2 1 modified
    @@ -66,7 +66,8 @@ Mautic.loadAjaxModal = function (target, route, method, header, footer, preventD
         //move the modal to the body tag to get around positioned div issues
         mQuery(target).one('show.bs.modal', function () {
             if (header) {
    -            mQuery(target + " .modal-title").html(header);
    +            // use text instead of html method to prevent XSS
    +            mQuery(target + " .modal-title").text(header);
             }
     
             if (footer && footer != 'false') {
    
  • app/bundles/DashboardBundle/Views/Dashboard/recentactivity.html.php+5 5 modified
    @@ -27,23 +27,23 @@
                 <div class="media-body">
                     <?php if (isset($log['userId']) && $log['userId']) : ?>
                         <a href="<?php echo $view['router']->path('mautic_user_action', ['objectAction' => 'edit', 'objectId' => $log['userId']]); ?>" data-toggle="ajax">
    -                        <?php echo $log['userName']; ?>
    +                        <?php echo $view->escape($log['userName']); ?>
                         </a>
                     <?php elseif ($log['userName']) : ?>
    -                    <?php echo $log['userName']; ?>
    +                    <?php echo $view->escape($log['userName']); ?>
                     <?php else: ?>
                         <?php echo $view['translator']->trans('mautic.core.system'); ?>
                     <?php endif; ?>
                     <?php echo $view['translator']->trans('mautic.dashboard.'.$log['action'].'.past.tense'); ?>
     
                     <?php if (!empty($log['route'])): ?>
                     <a href="<?php echo $log['route']; ?>" data-toggle="ajax">
    -                    <?php echo $log['objectName']; ?>
    +                    <?php echo $view->escape($log['objectName']); ?>
                     </a>
                     <?php elseif (!empty($log['objectName'])): ?>
    -                    <?php echo $log['objectName']; ?>
    +                    <?php echo $view->escape($log['objectName']); ?>
                     <?php endif; ?>
    -                <?php echo $log['object']; ?>
    +                <?php echo $view->escape($log['object']); ?>
                     <p class="fs-12 dark-sm"><small> <?php echo $view['date']->toFull($log['dateAdded']); ?></small></p>
                 </div>
             </li>
    
  • app/bundles/LeadBundle/Controller/LeadController.php+3 3 modified
    @@ -512,7 +512,7 @@ public function newAction()
                         $this->addFlash(
                             'mautic.core.notice.created',
                             [
    -                            '%name%'      => $identifier,
    +                            '%name%'      => htmlspecialchars($identifier, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false),
                                 '%menu_link%' => 'mautic_contact_index',
                                 '%url%'       => $this->generateUrl(
                                     'mautic_contact_action',
    @@ -706,7 +706,7 @@ public function editAction($objectId, $ignorePost = false)
                         $this->addFlash(
                             'mautic.core.notice.updated',
                             [
    -                            '%name%'      => $identifier,
    +                            '%name%'      => htmlspecialchars($identifier, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false),
                                 '%menu_link%' => 'mautic_contact_index',
                                 '%url%'       => $this->generateUrl(
                                     'mautic_contact_action',
    @@ -1114,7 +1114,7 @@ public function deleteAction($objectId)
                         'type'    => 'notice',
                         'msg'     => 'mautic.core.notice.deleted',
                         'msgVars' => [
    -                        '%name%' => $identifier,
    +                        '%name%' => htmlspecialchars($identifier, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false),
                             '%id%'   => $objectId,
                         ],
                     ];
    
  • app/bundles/LeadBundle/Views/Auditlog/details.html.php+0 1 modified
    @@ -99,4 +99,3 @@
             break;
     }
     echo $text;
    -echo '<!-- '.PHP_EOL.json_encode($details, JSON_PRETTY_PRINT).PHP_EOL.' -->';
    
  • app/bundles/LeadBundle/Views/Lead/form.html.php+1 1 modified
    @@ -11,7 +11,7 @@
     $view->extend('MauticCoreBundle:Default:content.html.php');
     $header = ($lead->getId()) ?
         $view['translator']->trans('mautic.lead.lead.header.edit',
    -        ['%name%' => $view['translator']->trans($lead->getPrimaryIdentifier())]) :
    +        ['%name%' => $view['translator']->trans($this->escape($lead->getPrimaryIdentifier()))]) :
         $view['translator']->trans('mautic.lead.lead.header.new');
     $view['slots']->set('headerTitle', $header);
     $view['slots']->set('mauticContent', 'lead');
    
  • app/bundles/PageBundle/Tests/Controller/PublicControllerFunctionalTest.php+44 0 added
    @@ -0,0 +1,44 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\PageBundle\Tests\Controller;
    +
    +use Mautic\CoreBundle\Test\MauticMysqlTestCase;
    +use PHPUnit\Framework\Assert;
    +
    +class PublicControllerFunctionalTest extends MauticMysqlTestCase
    +{
    +    public function testMtcEventCompanyXss(): void
    +    {
    +        $this->client->request('POST', '/mtc/event', [
    +            'page_url' => 'https://example.com?Company=%3Cimg+src+onerror%3Dalert%28%27Company%27%29%3E',
    +        ]);
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +
    +        $response = json_decode($clientResponse->getContent(), true);
    +
    +        $this->client->request('GET', sprintf('/s/contacts/view/%d', $response['id']));
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +
    +        $crawler        = $this->client->request('GET', sprintf('/s/contacts/edit/%d', $response['id']));
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +
    +        $buttonCrawlerNode = $crawler->selectButton('Save & Close');
    +        $form              = $buttonCrawlerNode->form();
    +        $this->client->submit($form);
    +        $clientResponse = $this->client->getResponse();
    +        Assert::assertTrue($clientResponse->isOk());
    +        $content = $clientResponse->getContent();
    +        Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
    +    }
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.