Feb 25, 2026·6 min read·8 visits
Mailpit <= 1.28.0 has an unauthenticated SSRF in its asset proxy. Attackers can use it to scan local ports (127.0.0.1) or hit cloud metadata endpoints. Fixed in 1.28.1.
A classic Server-Side Request Forgery (SSRF) vulnerability in Mailpit, the popular email testing tool for developers. The `/proxy` endpoint, intended to fetch remote images for email previews, failed to validate destination IP addresses. This allowed unauthenticated attackers to turn a benign development tool into a gateway for scanning internal networks, querying cloud metadata services, and accessing restricted APIs.
We all love Mailpit. It’s the spiritual successor to MailHog—a lightweight, Go-based SMTP server that developers spin up in Docker to catch emails during testing. It’s simple, it’s fast, and it has a nice web UI to view those "Password Reset" emails without actually spamming real users.
But here is the thing about development tools: they are often built with "functionality first, security later" mindsets. They live in trusted environments (localhost, internal dev clusters), so why bother with hardened security, right? Wrong.
To make the web UI look pretty, Mailpit needs to render HTML emails. Those emails often contain external images (tracking pixels, logos, cats). To avoid Mixed Content warnings or CORS issues when viewing these emails in the Mailpit UI, the developers added a proxy. A helper endpoint designed to fetch those remote assets on the server side and serve them back to your browser. And that is where the trouble begins.
The vulnerability lies in the /proxy (or /api/v1/proxy) endpoint. Its job is simple: take a URL from a query parameter, fetch it, and return the bits. A classic proxy.
The problem? It took its job too literally. The implementation in server/handlers/proxy.go checked if the URL started with http or https, but that was about it. It didn't check where that URL pointed.
> [!NOTE] > The Golden Rule of SSRF: If you take a URL from a user and fetch it, you must treat the destination IP as hostile until proven otherwise.
Mailpit failed to implement an IP denylist or a host allowlist. It didn't block loopback addresses (127.0.0.1), private RFC 1918 ranges (10.0.0.0/8), or link-local addresses. It simply resolved the DNS (if needed) and connected. This meant an attacker could ask Mailpit to fetch http://127.0.0.1:22 to see if SSH is open, or http://169.254.169.254 to steal AWS credentials.
Let's look at the logic that allowed this to happen. In the vulnerable versions (<= 1.28.0), the handler was effectively a blind relay.
// Pseudo-code of the vulnerable handler
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
targetUrl := r.URL.Query().Get("url")
// Weak validation: just checks scheme
if !strings.HasPrefix(targetUrl, "http") {
http.Error(w, "Invalid URL", 400)
return
}
// The vulnerability: No IP check, no Host check
resp, err := http.Get(targetUrl)
if err != nil {
// Error handling
}
// Stream response back to user
io.Copy(w, resp.Body)
}The fix in version 1.28.1 (Commit 3b9b470) completely refactored this. Instead of trusting a raw url parameter, the new implementation requires a data parameter containing a base64-encoded Message ID and URL.
<img src="..."> tag).Content-Type header of the upstream response. It only relays the data if it matches safe types (images, css, fonts). This prevents an attacker from reading JSON from internal APIs, even if they manage to inject a URL.Exploiting this is trivially easy. You don't need authentication. You just need network access to the Mailpit web port (usually 8025).
Here is how an attacker pivots through Mailpit to map an internal network:
If Mailpit is running in AWS, an attacker can steal the IAM role credentials attached to the instance.
GET /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: target-mailpit.comSince Mailpit often runs alongside other dev services, we can probe for them. For example, hitting Mailpit's own API from the inside (bypassing external firewalls if they exist).
GET /proxy?url=http://127.0.0.1:8025/api/v1/info HTTP/1.1
Host: target-mailpit.comIf the server returns JSON containing "Version", we know the host is up and accessible. We can then brute-force other ports (Redis on 6379, Postgres on 5432) to map the attack surface.
You might think, "It's just a dev tool, who cares?" But dev tools are often the bridge between the internet and the squishy internal network.
SSRF is a Gateway Drug: It turns a web server into a proxy for the attacker. In cloud environments, this leads to Account Takeover via metadata services. In on-premise networks, it leads to Network Mapping and accessing unauthenticated internal dashboards (like that Kibana instance you thought was safe behind the firewall).
Furthermore, because Mailpit is designed to capture emails, it often holds sensitive data (password reset links, PII in testing data). While this specific SSRF is an outbound vulnerability, the ability to interact with the localhost API could allow an attacker to delete messages or modify runtime configurations if the API lacks granular auth (which, let's be honest, it usually does in dev tools).
The immediate fix is simple: Update Mailpit to version 1.28.1 or later. The vendor responded quickly (within 24 hours) and implemented a robust fix that validates the context of the request.
However, rely on code fixes alone is risky. Here is how you should layer your defenses:
169.254.169.254 via local firewall rules.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Mailpit axllent | <= 1.28.0 | 1.28.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 |
| Attack Vector | Network |
| CVSS | 5.8 (Medium) |
| EPSS Score | 0.0113 |
| Exploit Status | PoC Available |
| Impact | Information Disclosure / Internal Scanning |
The application does not validate or incorrectly validates the destination IP address of a URL provided by a user, allowing the server to make requests to unexpected destinations.