Feb 26, 2026·5 min read·35 visits
Critical SSTI in listmonk versions < 5.0.2 allows authenticated users to read host-level environment variables (including secrets) by injecting Sprig helper functions into email templates.
In the world of self-hosted software, 'convenience' is often a polite synonym for 'security hole.' Listmonk, a high-performance newsletter manager written in Go, fell victim to this classic trade-off. By importing the entire Sprig template library without adequate filtering, the application allowed authenticated users to perform Server-Side Template Injection (SSTI). This wasn't just a minor UI glitch; it gave attackers a direct line to the server's environment variables, exposing database passwords, SMTP credentials, and AWS keys in cleartext.
Listmonk is the darling of the self-hosted marketing world. It’s fast, written in Go, and lets you send millions of emails without paying a cent to Mailchimp. To make emails dynamic—like greeting a user by name or formatting dates—it uses Go's native templating engine. But native Go templates are a bit bare-bones.
To spice things up, the developers integrated Sprig, a library that adds over 100 convenience functions to Go templates. Need to base64 encode a string? Sprig has you covered. Need to format a timestamp? Easy.
But here is the problem: Sprig also includes functions that have absolutely no business being accessible inside a marketing email template. Functions like env (read environment variables) and expandenv (expand system paths). By exposing the default Sprig function map to the user, listmonk effectively turned its template editor into a system shell's printenv command. It’s like installing a smart lock on your front door but leaving a key under the mat, and then broadcasting the location of the mat on a billboard.
The root cause of CVE-2025-49136 is a classic case of Implicit Trust. The developers trusted the Sprig library to be safe by default. In reality, Sprig is a general-purpose utility belt; it contains sharp knives alongside the harmless sporks.
When listmonk initialized the template engine for campaigns, it loaded the generic Sprig function map. This map includes:
os.Getenv: Wraps the system call to read environment variables.os.ExpandEnv: Replaces ${var} in strings with their environment values.In a secure context, like a CLI tool running on your laptop, these are fine. In a web application where users (even trusted marketing interns) define the template content, this is catastrophic. The application failed to curate this list, blindly passing the ability to read server memory into the hands of the user. It is a textbook breakdown of the Principle of Least Privilege.
Let's look at the smoking gun. The vulnerability existed because the code grabbed the default map and served it up raw. The fix, applied in version 5.0.2, is elegantly simple: it manually deletes the dangerous keys from the map before the template engine ever sees them.
Here is the diff from internal/manager/manager.go:
func (m *Manager) makeGnericFuncMap() template.FuncMap {
// ... existing setup ...
// VULNERABLE CODE (Conceptual Previous State)
// sprigFuncs := sprig.GenericFuncMap()
// maps.Copy(funcs, sprigFuncs)
// PATCHED CODE (v5.0.2)
sprigFuncs := sprig.GenericFuncMap()
// The fix: Explicitly nuke the dangerous functions
delete(sprigFuncs, "env")
delete(sprigFuncs, "expandenv")
maps.Copy(funcs, sprigFuncs)
return funcs
}> [!NOTE]
> This is a "allow-list via deny-list" approach. While effective here, a more robust security posture would be to explicitly allow only the functions you need (e.g., string manipulation and math) rather than trying to delete the ones you know are bad. Who knows what other weird functions Sprig might add in the future?
Exploiting this requires authenticated access, but don't let that lower your blood pressure. In many organizations, the person managing newsletters has weak credentials, no MFA, or is a third-party contractor. Once inside, the escalation to full infrastructure compromise is trivial.
The Attack Chain:
campaigns or templates permissions.<!-- The "I own your database" Payload -->
<p>Welcome to our newsletter!</p>
<p>Debug Info: {{ env "LISTMONK_db__password" }}</p>
<p>AWS Key: {{ env "AWS_SECRET_ACCESS_KEY" }}</p>os.Getenv("LISTMONK_db__password"), and renders the cleartext password right into the preview pane in your browser.If the attacker wants to be subtle, they could send a "Test Email" to themselves. The server will render the secrets into the email body and kindly deliver them to the attacker's inbox via the configured SMTP server.
This is not just about reading the newsletter configuration. Because listmonk is a modern, 12-factor app, it is almost exclusively configured via Environment Variables.
If you run listmonk in Kubernetes or Docker, the process environment likely contains:
CVSS 9.1 is arguably conservative. If your listmonk instance shares an environment with other services (e.g., a monolithic .env file mounted into multiple containers), this vulnerability compromises your entire stack.
The remediation is two-fold. First, stop the bleeding. Second, assume the patient has already lost blood.
Step 1: Update Software
Pull the latest Docker image or build from source. You need version v5.0.2 or higher. There is no config change required; the binary itself has been patched to disable the template functions.
Step 2: Rotate Secrets (Mandatory) If you were running a vulnerable version exposed to the internet (or untrusted internal users), you must treat your environment variables as compromised.
Simply patching the software closes the window, but it doesn't chase the burglar out of the house if they already stole the keys.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
listmonk knadh | >= 4.0.0, < 5.0.2 | 5.0.2 |
| Attribute | Detail |
|---|---|
| CWE | CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine) |
| Attack Vector | Network (Authenticated) |
| CVSS v3.1 | 9.1 (Critical) |
| Impact | Information Disclosure (High), System Compromise |
| Exploit Status | PoC Available |
| Architecture | Go (text/template + Sprig) |