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

XSS in contact/company tracking (no authentication)

CVE-2024-47050

Description

Prior to this patch being applied, Mautic's tracking was vulnerable to Cross-Site Scripting through the Page URL variable.

AI Insight

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

Mautic's tracking feature is vulnerable to stored Cross-Site Scripting (XSS) via unsanitized Page URL parameters.

Vulnerability

CVE-2024-47050 is a stored Cross-Site Scripting (XSS) vulnerability in Mautic's tracking functionality, where user-supplied data in the Page URL variable is not properly sanitized before rendering [1][2]. This allows an attacker to inject arbitrary HTML or JavaScript code that will be executed in the context of other users' browsers when they view the affected tracking data.

Exploitation

The vulnerability can be exploited by crafting a malicious URL that contains embedded script or HTML payloads, such as http://example.com"> [3][4]. When Mautic's tracking code processes this URL, it fails to escape the special characters, leading to the injection of malicious content into web pages that display the tracking data. No authentication is required from the attacker to inject the payload, but the attack relies on other users (e.g., administrators) viewing the affected tracking logs or reports.

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of a Mautic user's session. This can lead to session theft, data exfiltration, defacement, or phishing attacks. The worst-case scenario involves a privileged user viewing the malicious tracking data, granting the attacker access to sensitive marketing automation configurations, contacts, and campaigns.

Mitigation

The issue is patched in Mautic versions 5.1 and 4.4, as evidenced by two separate commits (one for each major version) that implement proper HTML entity encoding and URL sanitization [3][4]. Users are strongly advised to upgrade to the latest patched version immediately.

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/corePackagist
>= 2.6.0, < 4.4.134.4.13
mautic/corePackagist
>= 5.0.0-alpha, < 5.1.15.1.1
mautic/core-libPackagist
>= 2.6.0, < 4.4.134.4.13
mautic/core-libPackagist
>= 5.0.0-alpha, < 5.1.15.1.1

Affected products

3

Patches

2
43db5e492c0e

Merge remote-tracking branch 'security/Mst-76-m5' into 5.1

https://github.com/mautic/mauticJohn LinhartSep 18, 2024via ghsa
5 files changed · +136 7
  • app/bundles/CoreBundle/Tests/Unit/Twig/Helper/FormatterHelperTest.php+100 0 modified
    @@ -119,4 +119,104 @@ public static function stringProvider(): iterable
                 \DateTime::createFromFormat('Y-m-d H:i:s', 'now', new \DateTimeZone('UTC')),
             ];
         }
    +
    +    /**
    +     * @dataProvider urlFormatProvider
    +     */
    +    public function testUrlFormat(string $url, string $expected): void
    +    {
    +        $result = $this->formatterHelper->_($url, 'url');
    +        $this->assertEquals($expected, $result);
    +    }
    +
    +    /**
    +     * @return array<string, array<string>>
    +     */
    +    public function urlFormatProvider(): array
    +    {
    +        return [
    +            'normal url' => [
    +                'http://example.com',
    +                '<a href="http://example.com" target="_blank">http://example.com</a>',
    +            ],
    +            'malicious url' => [
    +                'http://example.com"><script>alert("XSS")</script>',
    +                '<a href="http://example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;" target="_blank">http://example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;</a>',
    +            ],
    +            'malicious url2' => [
    +                'http://example.com?a="<b>test</b>',
    +                '<a href="http://example.com?a=%22%3Cb%3Etest%3C%2Fb%3E" target="_blank">http://example.com?a=%22%3Cb%3Etest%3C%2Fb%3E</a>',
    +            ],
    +            'url with single GET parameter' => [
    +                'http://example.com/page?param=value',
    +                '<a href="http://example.com/page?param=value" target="_blank">http://example.com/page?param=value</a>',
    +            ],
    +            'url with multiple GET parameters' => [
    +                'http://example.com/search?q=test&page=1&sort=desc',
    +                '<a href="http://example.com/search?q=test&page=1&sort=desc" target="_blank">http://example.com/search?q=test&page=1&sort=desc</a>',
    +            ],
    +            'url with encoded GET parameters' => [
    +                'http://example.com/search?q=hello+world&lang=en',
    +                '<a href="http://example.com/search?q=hello+world&lang=en" target="_blank">http://example.com/search?q=hello+world&lang=en</a>',
    +            ],
    +            'url with special characters in GET parameters' => [
    +                'http://example.com/path?param=value&special=!@#$%^&*()',
    +                '<a href="http://example.com/path?param=value&special=%21%40#$%^&*()" target="_blank">http://example.com/path?param=value&special=%21%40#$%^&*()</a>',
    +            ],
    +            'https url' => [
    +                'https://secure.example.com',
    +                '<a href="https://secure.example.com" target="_blank">https://secure.example.com</a>',
    +            ],
    +            'url with port number' => [
    +                'http://example.com:8080/path',
    +                '<a href="http://example.com:8080/path" target="_blank">http://example.com:8080/path</a>',
    +            ],
    +            'url with username and password' => [
    +                'http://user:pass@example.com',
    +                '<a href="http://user:pass@example.com" target="_blank">http://user:pass@example.com</a>',
    +            ],
    +            'url with fragment identifier' => [
    +                'http://example.com/page#section',
    +                '<a href="http://example.com/page#section" target="_blank">http://example.com/page#section</a>',
    +            ],
    +        ];
    +    }
    +
    +    /**
    +     * @dataProvider emailFormatProvider
    +     */
    +    public function testEmailFormat(string $email, string $expected): void
    +    {
    +        $result = $this->formatterHelper->_($email, 'email');
    +        $this->assertEquals($expected, $result);
    +    }
    +
    +    /**
    +     * @return array<string, array<string>>
    +     */
    +    public function emailFormatProvider(): array
    +    {
    +        return [
    +            'normal email' => [
    +                'user@example.com',
    +                '<a href="mailto:user@example.com">user@example.com</a>',
    +            ],
    +            'email with alias' => [
    +                'user.one+test@example.com',
    +                '<a href="mailto:user.one+test@example.com">user.one+test@example.com</a>',
    +            ],
    +            'malicious email' => [
    +                'user@example.com"><script>alert("XSS")</script>',
    +                '<a href="mailto:user@example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;">user@example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;</a>',
    +            ],
    +        ];
    +    }
    +
    +    public function testArrayFormat(): void
    +    {
    +        $input    = ['<script>alert("XSS")</script>'];
    +        $result   = $this->formatterHelper->_($input, 'array');
    +        $expected = '&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;';
    +        $this->assertEquals($expected, $result);
    +    }
     }
    
  • app/bundles/CoreBundle/Twig/Helper/FormatterHelper.php+5 3 modified
    @@ -47,7 +47,7 @@ public function _($val, $type = 'html', $textOnly = false, $round = 1)
                         if (is_array($v)) {
                             $stringParts = $this->_($v, 'array', $textOnly, $round + 1);
                         } else {
    -                        $stringParts[] = $v;
    +                        $stringParts[] = InputHelper::clean($v);
                         }
                     }
                     if (1 === $round) {
    @@ -66,10 +66,12 @@ public function _($val, $type = 'html', $textOnly = false, $round = 1)
                     $string = $this->dateHelper->toDate($val, 'utc');
                     break;
                 case 'url':
    -                $string = ($textOnly) ? $val : '<a href="'.$val.'" target="_new">'.$val.'</a>';
    +                $url        = InputHelper::url($val);
    +                $string     = ($textOnly) ? $url : '<a href="'.$url.'" target="_blank">'.$url.'</a>';
                     break;
                 case 'email':
    -                $string = ($textOnly) ? $val : '<a href="mailto:'.$val.'">'.$val.'</a>';
    +                $url    = InputHelper::url($val);
    +                $string = ($textOnly) ? $url : '<a href="mailto:'.$url.'">'.$url.'</a>';
                     break;
                 case 'int':
                     $string = strval((int) $val);
    
  • app/bundles/ReportBundle/Resources/views/Report/_details_report_content.html.twig+3 3 modified
    @@ -82,11 +82,11 @@
                                                     {% elseif 'date' is same as cellType %}
                                                         {{ dateToShort(cellVal, 'UTC') }}
                                                     {% else %}
    -                                                    {% set value = format(cellVal, cellType) %}
    +                                                    {% set value = format(cellVal, cellType)|purify %}
                                                         {% if collapse == false %}
    -                                                        {{ format(cellVal, cellType) }}
    +                                                        {{ format(cellVal, cellType)|purify }}
                                                         {% else %}
    -                                                        {% set value = htmlEntityDecode(format(cellVal, cellType)) %}
    +                                                        {% set value = htmlEntityDecode(format(cellVal, cellType)|purify) %}
                                                             <div data-toggle="collapse" data-target="#audit-log-details-{{ startCount }}" class="accordion-toggle" style="cursor: pointer">
                                                                 {{ value|length > 50 ? value|slice(0, 50) ~ '...' : value }}
                                                             </div>
    
  • app/bundles/ReportBundle/Resources/views/Report/export.html.twig+1 1 modified
    @@ -53,7 +53,7 @@
                       <tr>
                           <td>{{ count + 1 }}</td>
                           {% for k, v in data %}
    -                          <td>{{ format(v, reportDataResult.getType(k)) }}</td>
    +                          <td>{{ format(v, reportDataResult.getType(k))|purify }}</td>
                           {% endfor %}
                       </tr>
                   {% endfor %}
    
  • app/bundles/ReportBundle/Tests/Controller/ReportControllerFunctionalTest.php+27 0 modified
    @@ -424,6 +424,33 @@ public function testDescriptionIsNotEscaped(): void
             $this->assertStringContainsString('<small><b>This is allowed HTML</b></small>', $clientResponseContent);
         }
     
    +    public function testXssUrlFromQuery(): void
    +    {
    +        $report = new Report();
    +        $report->setName('Hits report');
    +        $report->setDescription('<b>Text Xss Hits</b>');
    +        $report->setSource('page.hits');
    +        $coulmns = [
    +            'ph.isp',
    +            'ph.url',
    +            'ph.browser_languages',
    +            'ph.referer',
    +            'ph.remote_host',
    +            'ph.user_agent',
    +        ];
    +        $report->setColumns($coulmns);
    +        $this->getContainer()->get('mautic.report.model.report')->saveEntity($report);
    +        $xssHeader     = '<script>alert(1)</script>';
    +        $this->client->request('GET', '/mtracking.gif?page_url='.$xssHeader);
    +        $this->assertResponseStatusCodeSame(200);
    +        $this->client->request('GET', '/s/reports/view/'.$report->getId());
    +        $this->assertResponseStatusCodeSame(200);
    +        $this->assertStringNotContainsString($xssHeader, $this->client->getResponse()->getContent());
    +
    +        $this->client->request('GET', '/s/reports/view/'.$report->getId().'/export/html');
    +        $this->assertStringNotContainsString($xssHeader, $this->client->getResponse()->getContent());
    +    }
    +
         /**
          * @param string[] $graphs
          */
    
0f21a3aa9c89

Merge remote-tracking branch 'security/Mst-76-m4' into 4.4

https://github.com/mautic/mauticJohn LinhartSep 18, 2024via ghsa
3 files changed · +132 3
  • app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php+5 3 modified
    @@ -55,7 +55,7 @@ public function _($val, $type = 'html', $textOnly = false, $round = 1)
                         if (is_array($v)) {
                             $stringParts = $this->_($v, 'array', $textOnly, $round + 1);
                         } else {
    -                        $stringParts[] = $v;
    +                        $stringParts[] = InputHelper::clean($v);
                         }
                     }
                     if (1 === $round) {
    @@ -74,10 +74,12 @@ public function _($val, $type = 'html', $textOnly = false, $round = 1)
                     $string = $this->dateHelper->toDate($val, 'utc');
                     break;
                 case 'url':
    -                $string = ($textOnly) ? $val : '<a href="'.$val.'" target="_new">'.$val.'</a>';
    +                $url    = InputHelper::url($val);
    +                $string = ($textOnly) ? $url : '<a href="'.$url.'" target="_blank">'.$url.'</a>';
                     break;
                 case 'email':
    -                $string = ($textOnly) ? $val : '<a href="mailto:'.$val.'">'.$val.'</a>';
    +                $url    = InputHelper::url($val);
    +                $string = ($textOnly) ? $url : '<a href="mailto:'.$url.'">'.$url.'</a>';
                     break;
                 case 'int':
                     $string = (int) $val;
    
  • app/bundles/CoreBundle/Tests/Unit/Templating/Helper/FormatterHelperTest.php+100 0 modified
    @@ -104,4 +104,104 @@ public function stringProvider(): iterable
                 DateTime::createFromFormat('Y-m-d H:i:s', 'now', new \DateTimeZone('UTC')),
             ];
         }
    +
    +    /**
    +     * @dataProvider urlFormatProvider
    +     */
    +    public function testUrlFormat(string $url, string $expected): void
    +    {
    +        $result = $this->formatterHelper->_($url, 'url');
    +        $this->assertEquals($expected, $result);
    +    }
    +
    +    /**
    +     * @return array<string, array<string>>
    +     */
    +    public function urlFormatProvider(): array
    +    {
    +        return [
    +            'normal url' => [
    +                'http://example.com',
    +                '<a href="http://example.com" target="_blank">http://example.com</a>',
    +            ],
    +            'malicious url' => [
    +                'http://example.com"><script>alert("XSS")</script>',
    +                '<a href="http://example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;" target="_blank">http://example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;</a>',
    +            ],
    +            'malicious url2' => [
    +                'http://example.com?a="<b>test</b>',
    +                '<a href="http://example.com?a=%22%3Cb%3Etest%3C%2Fb%3E" target="_blank">http://example.com?a=%22%3Cb%3Etest%3C%2Fb%3E</a>',
    +            ],
    +            'url with single GET parameter' => [
    +                'http://example.com/page?param=value',
    +                '<a href="http://example.com/page?param=value" target="_blank">http://example.com/page?param=value</a>',
    +            ],
    +            'url with multiple GET parameters' => [
    +                'http://example.com/search?q=test&page=1&sort=desc',
    +                '<a href="http://example.com/search?q=test&page=1&sort=desc" target="_blank">http://example.com/search?q=test&page=1&sort=desc</a>',
    +            ],
    +            'url with encoded GET parameters' => [
    +                'http://example.com/search?q=hello+world&lang=en',
    +                '<a href="http://example.com/search?q=hello+world&lang=en" target="_blank">http://example.com/search?q=hello+world&lang=en</a>',
    +            ],
    +            'url with special characters in GET parameters' => [
    +                'http://example.com/path?param=value&special=!@#$%^&*()',
    +                '<a href="http://example.com/path?param=value&special=%21%40#$%^&*()" target="_blank">http://example.com/path?param=value&special=%21%40#$%^&*()</a>',
    +            ],
    +            'https url' => [
    +                'https://secure.example.com',
    +                '<a href="https://secure.example.com" target="_blank">https://secure.example.com</a>',
    +            ],
    +            'url with port number' => [
    +                'http://example.com:8080/path',
    +                '<a href="http://example.com:8080/path" target="_blank">http://example.com:8080/path</a>',
    +            ],
    +            'url with username and password' => [
    +                'http://user:pass@example.com',
    +                '<a href="http://user:pass@example.com" target="_blank">http://user:pass@example.com</a>',
    +            ],
    +            'url with fragment identifier' => [
    +                'http://example.com/page#section',
    +                '<a href="http://example.com/page#section" target="_blank">http://example.com/page#section</a>',
    +            ],
    +        ];
    +    }
    +
    +    /**
    +     * @dataProvider emailFormatProvider
    +     */
    +    public function testEmailFormat(string $email, string $expected): void
    +    {
    +        $result = $this->formatterHelper->_($email, 'email');
    +        $this->assertEquals($expected, $result);
    +    }
    +
    +    /**
    +     * @return array<string, array<string>>
    +     */
    +    public function emailFormatProvider(): array
    +    {
    +        return [
    +            'normal email' => [
    +                'user@example.com',
    +                '<a href="mailto:user@example.com">user@example.com</a>',
    +            ],
    +            'email with alias' => [
    +                'user.one+test@example.com',
    +                '<a href="mailto:user.one+test@example.com">user.one+test@example.com</a>',
    +            ],
    +            'malicious email' => [
    +                'user@example.com"><script>alert("XSS")</script>',
    +                '<a href="mailto:user@example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;">user@example.com&#34;&#62;&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;</a>',
    +            ],
    +        ];
    +    }
    +
    +    public function testArrayFormat(): void
    +    {
    +        $input    = ['<script>alert("XSS")</script>'];
    +        $result   = $this->formatterHelper->_($input, 'array');
    +        $expected = '&#60;script&#62;alert(&#34;XSS&#34;)&#60;/script&#62;';
    +        $this->assertEquals($expected, $result);
    +    }
     }
    
  • app/bundles/ReportBundle/Tests/Controller/ReportControllerFunctionalTest.php+27 0 modified
    @@ -54,4 +54,31 @@ public function testContactReportSqlInjectionDontWork(): void
             $this->client->request('GET', '/s/reports/view/'.$report->getId().'?tmpl=list&name=report.'.$report->getId().'&orderby=a_id');
             $this->assertTrue($this->client->getResponse()->isOk());
         }
    +
    +    public function testXssUrlFromQuery(): void
    +    {
    +        $report = new Report();
    +        $report->setName('Hits report');
    +        $report->setDescription('<b>Text Xss Hits</b>');
    +        $report->setSource('page.hits');
    +        $coulmns = [
    +            'ph.isp',
    +            'ph.url',
    +            'ph.browser_languages',
    +            'ph.referer',
    +            'ph.remote_host',
    +            'ph.user_agent',
    +        ];
    +        $report->setColumns($coulmns);
    +        $this->getContainer()->get('mautic.report.model.report')->saveEntity($report);
    +        $xssHeader     = '<script>alert(1)</script>';
    +        $this->client->request('GET', '/mtracking.gif?page_url='.$xssHeader);
    +        $this->assertResponseStatusCodeSame(200);
    +        $this->client->request('GET', '/s/reports/view/'.$report->getId());
    +        $this->assertResponseStatusCodeSame(200);
    +        $this->assertStringNotContainsString($xssHeader, $this->client->getResponse()->getContent());
    +
    +        $this->client->request('GET', '/s/reports/view/'.$report->getId().'/export/html');
    +        $this->assertStringNotContainsString($xssHeader, $this->client->getResponse()->getContent());
    +    }
     }
    

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.