XSS in contact/company tracking (no authentication)
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.
| Package | Affected versions | Patched versions |
|---|---|---|
mautic/corePackagist | >= 2.6.0, < 4.4.13 | 4.4.13 |
mautic/corePackagist | >= 5.0.0-alpha, < 5.1.1 | 5.1.1 |
mautic/core-libPackagist | >= 2.6.0, < 4.4.13 | 4.4.13 |
mautic/core-libPackagist | >= 5.0.0-alpha, < 5.1.1 | 5.1.1 |
Affected products
3- ghsa-coords2 versions
>= 2.6.0, < 4.4.13+ 1 more
- (no CPE)range: >= 2.6.0, < 4.4.13
- (no CPE)range: >= 2.6.0, < 4.4.13
- Mautic/Mauticv5Range: >= 2.6.0
Patches
243db5e492c0eMerge remote-tracking branch 'security/Mst-76-m5' into 5.1
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"><script>alert("XSS")</script>" target="_blank">http://example.com"><script>alert("XSS")</script></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"><script>alert("XSS")</script>">user@example.com"><script>alert("XSS")</script></a>', + ], + ]; + } + + public function testArrayFormat(): void + { + $input = ['<script>alert("XSS")</script>']; + $result = $this->formatterHelper->_($input, 'array'); + $expected = '<script>alert("XSS")</script>'; + $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 */
0f21a3aa9c89Merge remote-tracking branch 'security/Mst-76-m4' into 4.4
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"><script>alert("XSS")</script>" target="_blank">http://example.com"><script>alert("XSS")</script></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"><script>alert("XSS")</script>">user@example.com"><script>alert("XSS")</script></a>', + ], + ]; + } + + public function testArrayFormat(): void + { + $input = ['<script>alert("XSS")</script>']; + $result = $this->formatterHelper->_($input, 'array'); + $expected = '<script>alert("XSS")</script>'; + $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- github.com/advisories/GHSA-73gr-32wg-qhh7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-47050ghsaADVISORY
- github.com/mautic/mautic/commit/0f21a3aa9c896788e1986fae0d7f166fc7a14c30ghsaWEB
- github.com/mautic/mautic/commit/43db5e492c0ef82c917745849d5b454dbc8ca2c4ghsaWEB
- github.com/mautic/mautic/security/advisories/GHSA-73gr-32wg-qhh7ghsaWEB
News mentions
0No linked articles in our index yet.