Feb 26, 2026·6 min read·4 visits
Mailpit versions prior to 1.29.2 contain a logic flaw in the 'Link Check' feature. The application fails to validate if a URL points to a local or private IP address before initiating a connection. This allows an attacker to use the Mailpit server as a proxy to scan the internal network (localhost, 10.x.x.x) or access cloud instance metadata (AWS/GCP), simply by sending an email containing a crafted link.
A critical Server-Side Request Forgery (SSRF) vulnerability in Mailpit's Link Check API allows unauthenticated remote attackers to map internal networks and enumerate cloud metadata. By injecting malicious URLs into emails and triggering the application's automated link verification, attackers can force the server to issue HTTP requests to arbitrary destinations, bypassing network segmentation.
Mailpit is the darling of the developer world. It is the modern, Go-based successor to MailHog—a tool designed to catch emails sent by your application during development so you don't accidentally spam your entire customer database while testing a password reset flow. It sits quietly in your staging environment or on your local machine, capturing SMTP traffic and presenting it in a nice web UI.
One of Mailpit's "helpful" features is the Link Checker. The idea is sound: developers want to know if the links in their marketing emails are broken before they go live. Mailpit parses the email, finds the anchor tags, and offers an API endpoint to verify them. It creates a convenient dashboard showing which links return 200 OK and which return 404 Not Found.
But here is the catch: to check a link, Mailpit has to visit the link. In security terms, the application acts as an HTTP proxy. If you don't strictly control where that proxy is allowed to go, you have just handed an attacker a skeleton key to your internal network. You built a tool to catch mail, but CVE-2026-27808 turns it into a tool to catch shells.
The vulnerability resides in internal/linkcheck/status.go. The function doHead() was responsible for performing the connectivity check. In earlier versions of Mailpit, the developers utilized Go's standard http.Client with a default transport configuration.
Go's default HTTP client is famously promiscuous. It does exactly what you ask it to do. If you ask it to fetch http://google.com, it goes to Google. If you ask it to fetch http://localhost:22, it tries to talk HTTP to your SSH daemon. If you ask it to fetch http://169.254.169.254/latest/meta-data/, it happily talks to the AWS metadata service.
The flaw wasn't a buffer overflow or a complex race condition; it was a lack of boundary validation. The code blindly trusted the URLs extracted from the email body. It failed to implement an "allowlist" of permitted schemes or a "blocklist" of private IP ranges (RFC 1918). It essentially treated user input (the email content) as a trusted command to initiate network traffic.
To fix an SSRF in Go, you cannot simply verify the URL string. Attackers can use DNS rebinding or alternative IP formats (e.g., 2130706433 for 127.0.0.1) to bypass string matching. The only robust way to fix this is to hook into the Dialer—the component that actually opens the TCP connection.
The patch in version 1.29.2 introduces a safeDialContext. Here is the logic breakdown of the fix:
1. Resolve First, Connect Second Instead of letting the HTTP client handle DNS, the fix manually resolves the hostname to an IP address first.
2. The Blocklist
The resolved IPs are checked against a strict list of forbidden ranges using a new IsInternalIP helper. This blocks Loopback (127.0.0.0/8), Private Networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12), and Link-Local addresses (169.254.0.0/16).
// Simplified view of the patch logic
func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, _ := net.SplitHostPort(addr)
// Step 1: Resolve IP manually
ips, _ := net.DefaultResolver.LookupIPAddr(ctx, host)
for _, ip := range ips {
// Step 2: Check blocklist
if IsInternalIP(ip.IP) {
return nil, errors.New("blocked internal IP")
}
}
// Step 3: Dial the SPECIFIC VALIDATED IP, not the hostname
// This prevents DNS Rebinding attacks.
return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port))
}3. Redirect Handling
The fix also includes a CheckRedirect hook. This is critical because a safe external URL (e.g., http://attacker.com/safe) could redirect to http://127.0.0.1/secret, bypassing the initial check. The patch ensures the safe dialer is used for every hop in the chain.
Exploiting this requires no authentication in default configurations. The attack vector works because Mailpit processes emails received via SMTP, and then exposes the Link Check results via HTTP API.
Phase 1: The Setup The attacker connects to the Mailpit SMTP port (usually 1025) or sends an email through an application that relays to Mailpit. The email body contains HTML with links targeting the internal infrastructure.
Subject: Trojan Horse
Check out these "harmless" links:
<a href="http://127.0.0.1:6379">Local Redis</a>
<a href="http://169.254.169.254/latest/meta-data/">AWS Metadata</a>
<a href="http://10.0.0.5:9200">Internal ElasticSearch</a>Phase 2: The Trigger The attacker queries the Mailpit API list endpoint to find the ID of the email they just sent. Then, they trigger the vulnerability:
GET /api/v1/message/{MESSAGE_ID}/link-check
Phase 3: The Oracle
Mailpit attempts to HEAD these URLs. The JSON response will contain the status of each link.
127.0.0.1:6379 returns a connection error, Redis isn't there.169.254.169.254 returns 200 OK, the attacker knows they are in a cloud environment and can proceed to enumerate role names and permissions.Because the API returns specific error messages and status codes, this acts as a high-speed internal port scanner.
While CVSS 5.8 might look "Medium," the contextual severity is often Critical. Mailpit is almost exclusively deployed in development and staging environments. These environments are notoriously lax on security.
1. Cloud Identity Theft: If Mailpit is running in AWS/GCP/Azure, this SSRF allows an attacker to steal the temporary credentials assigned to the instance. This often leads to full account compromise if the instance has over-privileged IAM roles (e.g., S3 full access).
2. Unauthenticated Internal Services: Developers often run databases like Redis, MongoDB, or Elasticsearch without authentication on localhost because "it's just dev." This vulnerability bridges the gap from the outside world directly to those open ports.
3. CI/CD Pipeline Poisoning: If Mailpit is running inside a CI pipeline, accessing the internal network might allow an attacker to interfere with build artifacts or steal source code.
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.29.2 | 1.29.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 (Server-Side Request Forgery) |
| Attack Vector | Network (API Triggered) |
| CVSS v3.1 | 5.8 (Medium) |
| EPSS Score | 0.047% |
| Impact | Internal Reconnaissance / Metadata Exposure |
| Exploit Status | PoC Available |