Mar 1, 2026·5 min read·14 visits
listmonk contains a Stored XSS vulnerability in its campaign preview feature. Attackers with campaign management rights can inject malicious scripts that execute in the administrator's browser, leading to full Account Takeover (ATO). The issue is resolved in version 6.0.0.
A critical Stored Cross-Site Scripting (XSS) vulnerability exists in listmonk versions 5.1.0 and prior, originating from the campaign and template management modules. The flaw allows authenticated users with limited permissions to inject arbitrary JavaScript via unsanitized template fields. Execution occurs when an administrator views a campaign preview or the public archive, bypassing the same-origin policy within the administrative dashboard. Successful exploitation permits attackers to hijack the administrator's session, enabling unauthorized API calls such as creating new super-admin users.
listmonk, a self-hosted newsletter and mailing list manager written in Go and Vue, contains a Stored Cross-Site Scripting (XSS) vulnerability in its campaign and template editing interfaces. The vulnerability (CWE-79) arises because the application permits users with campaigns:manage or templates:manage permissions to save content containing arbitrary HTML and JavaScript.
The core issue manifests when this stored content is rendered. Specifically, the application failed to adequately sanitize input server-side and relied on a frontend rendering context that did not enforce strict isolation. When a privileged user, such as a Super Admin, initiates a "Preview" of a compromised campaign or views the public archive, the injected script executes within their authenticated session context.
While the CVSS base score is calculated at 5.4 (Medium) due to the requirement for privileges and user interaction, the practical impact is Critical. The vulnerability facilitates a direct path to Administrative Account Takeover (ATO), allowing attackers to pivot from a low-privileged editor account to full system control.
The vulnerability stems from two concurring architectural deficiencies regarding input handling and output rendering.
1. Unsafe Template Handling: The Go html/template package provides contextual auto-escaping to prevent XSS. However, listmonk exposed a mechanism to bypass this protection, likely via a custom template filter (e.g., Safe). This filter casts raw strings to template.HTML, a type that instructs the Go template engine to treat the content as trusted and render it without escaping. Attackers utilized this to inject raw HTML payloads.
2. Lack of Context Isolation: The campaign preview feature rendered user-controlled HTML directly within the administrative dashboard's Document Object Model (DOM) or a non-sandboxed context. Without the sandbox attribute on the iframe used for previews, the rendered content retained access to the parent window's context, including cookies, local storage, and the ability to issue XHR/Fetch requests on behalf of the logged-in administrator.
The remediation strategy focused on isolating the rendering context rather than attempting to sanitize complex HTML, which is error-prone for newsletter software that requires rich layout capabilities. The fix was implemented in frontend/src/views/Campaigns/Preview.vue (and related files) by enforcing strict sandboxing on the preview iframe.
The following diff illustrates the critical change in commit 74dc5a01:
<!-- Vulnerable Implementation (Conceptual) -->
<iframe
:srcdoc="campaignContent"
class="preview-frame"
></iframe>
<!-- Patched Implementation (Commit 74dc5a01) -->
<iframe
:srcdoc="campaignContent"
class="preview-frame"
sandbox="allow-scripts"
></iframe>By adding sandbox="allow-scripts", the browser treats the content within the iframe as being from a unique origin. While scripts can still run (necessary for rendering some dynamic email components), they are blocked from accessing the parent document's DOM (hosting the admin session) or making same-origin requests to the API. This effectively neutralizes the ATO vector even if malicious scripts are injected.
Additional hardening in version 6.0.0 included removing the getHostByName sprig function (Commit 39658c44) to prevent DNS reconnaissance and implementing stricter CORS policies (Commit cdf0a5c1).
An attacker with campaigns:manage permissions can exploit this vulnerability using the following workflow:
Injection: The attacker creates a new campaign or edits an existing template. In the body content, they inject a JavaScript payload designed to create a new administrative user. A typical payload might look like:
<script>
fetch('/api/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: 'hacker', password: 'Password123!', role: 'admin'})
});
</script>Staging: The attacker saves the campaign. The payload is now stored in the database.
Trigger: The attacker waits for an administrator to view the campaign list and click "Preview" to check the draft, or sends a link to the public archive view.
Execution: Upon rendering, the script executes in the admin's browser. Because the admin has an active session, the fetch request is authenticated automatically by the browser sending the session cookies.
Takeover: The API processes the request, creating the rogue admin account. The attacker can now log in with full privileges.
The impact of this vulnerability extends beyond simple script execution to complete system compromise.
Although NVD rates this as Medium (5.4), the business impact is Critical due to the Account Takeover (ATO) capability. The S:C (Scope Changed) vector component correctly identifies that the vulnerability affects resources beyond the immediate component (the user's browser/session).
The primary mitigation is to upgrade listmonk to version 6.0.0 or later immediately. This version includes the iframe sandboxing fix, CORS hardening, and the removal of dangerous template functions.
Temporary Workarounds: If an immediate upgrade is not feasible:
campaigns:manage and templates:manage permissions from all untrusted users. Only fully trusted administrators should be allowed to edit content./api/campaigns containing <script> tags or {{.*|.*Safe.*}} patterns in the request body.Verification: After patching, administrators should inspect the User list (/admin/users) for any unrecognized accounts created prior to the update.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
listmonk knadh | <= 5.1.0 | 6.0.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS Score | 5.4 (Medium) |
| Impact | Account Takeover (ATO) |
| EPSS Score | 0.0001 |
| Exploit Status | PoC Available |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')