Feb 21, 2026·5 min read·38 visits
The Lettermint SDK failed to reset its internal state after sending an email. If you reused the client instance (a common pattern in Node.js), the attachments and metadata from Email A would be silently attached to Email B. Fixed in v1.5.1.
A state management vulnerability in the Lettermint Node.js SDK allows sensitive email data to persist across transactions when the client is reused. Due to an implementation flaw in the fluent interface design, properties like attachments, CCs, and headers were not cleared after sending, causing them to bleed into subsequent email requests. This effectively turns a shared client instance into a data leakage hose.
Developers love fluent interfaces. You know the type: object.doThis().thenThat().finalize(). It reads like English, looks clean in a pull request, and makes us feel like sophisticated architects. But in the world of synchronous state management, fluent interfaces are often a concealed bear trap waiting to snap on your ankle.
In the case of CVE-2026-27492, the lettermint-node SDK fell victim to this exact pattern. The library was designed to let developers build emails incrementally. You create a client, you add a recipient, you attach a file, and you hit send. It’s elegant, until you realize how the sausage is made underneath.
The vulnerability isn't a buffer overflow or a deserialization gadget. It's a logic error born from a fundamental misunderstanding of object lifecycles in a long-running Node.js process. When you treat a stateful object (the email builder) as a stateless service (the API client), things get messy fast.
Imagine a restaurant where the busboy never clears the table. The first customer leaves their half-eaten burger and a signed credit card receipt. The second customer sits down, orders a salad, and unwittingly inherits the burger and the credit card details. This is exactly what was happening inside lettermint-node.
The EmailEndpoint class maintained a persistent internal object called this.payload. When you called methods like .to() or .attach(), it modified this internal state. Crucially, the .send() method—the logical conclusion of the transaction—did not wipe the slate clean.
If a developer instantiated the client once (globally) and reused it for performance reasons—a standard best practice in Node.js to manage connection pooling—the SDK became a toxic cache. The payload object would simply keep accumulating data. A 'Password Reset' email sent to User A might unknowingly include the 'Invoice.pdf' attached to the previous email sent to User B. It wasn't just leaking metadata; it was cross-contaminating entire data streams.
Let's look at the smoking gun. The vulnerable code relied on a persistent mutation of this.payload. Here is a simplified view of the architecture prior to version 1.5.1:
// Vulnerable Logic
class EmailEndpoint {
constructor() {
// this.payload is created once per instance
this.payload = {};
}
attach(file) {
this.payload.attachments.push(file);
return this;
}
async send() {
// Sends the current state of payload
await httpClient.post('/send', this.payload);
// ERROR: The payload is never cleared here!
}
}The fix seems obvious in hindsight, but the implementation details matter. The vendor didn't just need to clear the data; they needed to ensure it was cleared even if the network request failed. If the cleanup only happened on a successful 200 OK, a timeout or 500 error would leave the object dirty, poisoning the retry or the next request.
The patch (Commit 83343f1) introduced a finally block to guarantee sanitation:
// The Fix in v1.5.1
async send() {
try {
return await this.httpClient.post('/send', this.payload);
} finally {
// This runs success or fail
this.reset();
}
}
private reset() {
this.payload = { from: '', to: [], subject: '' };
}This forces a hard reset of the internal state after every attempt, essentially flushing the buffer.
To exploit this, we don't need to send malicious packets. We just need to rely on the target application's laziness. We are looking for a Node.js backend (Express, Fastify, NestJS) where the Lettermint client is defined as a global constant or a singleton service.
Here is how an attacker might verify the leak in a black-box scenario. Suppose the target application sends a confirmation email when you update your profile, and a different email when you request a data export.
But the real danger is Inter-User Leakage. If User A triggers the heavy email, and User B triggers the light email milliseconds later on the same server process, User B receives User A's data.
Here is the part the advisory glosses over: Version 1.5.1 fixes sequential reuse, but it does NOT fix concurrent reuse.
Even with the reset() in the finally block, this SDK is fundamentally thread-unsafe (or rather, "async-unsafe"). Node.js is single-threaded, but it is asynchronous. If an application handles two requests concurrently and awaits I/O (like a database call) while building the email, the operations can interleave.
Consider this flow:
sdk.to('admin').awaits a database query.sdk.to('hacker') on the same instance.sdk.send().Because this.payload is shared, Request A will now send the email to both 'admin' and 'hacker'. The patch cleans up after the send, but it does nothing to prevent race conditions during the build process. The only true fix is to never, ever store request-specific state on a global object.
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
lettermint-node Lettermint | <= 1.5.0 | 1.5.1 |
| Attribute | Detail |
|---|---|
| CWE | CWE-488 (Data Element to Wrong Session) |
| CVSS | 4.7 (Medium) |
| Attack Vector | Local (Context Dependent) |
| Exploit Maturity | PoC Available |
| Authentication | Single Instance Reuse Required |
| Impact | Data Leakage / PII Exposure |
Exposure of Data Element to Wrong Session
A state persistence vulnerability exists in Tornado's CurlAsyncHTTPClient component where pooled pycurl.Curl handles are reused across asynchronous requests without a complete state reset. Consequently, sensitive per-request configurations, such as client TLS certificates or proxy basic authentication credentials, persist on the shared handle. This behavior leads to subsequent requests leaking these credentials to unauthorized remote servers.
CVE-2026-48748 is a denial-of-service vulnerability in Netty's HTTP/3 codec (netty-codec-http3) occurring when QPACK dynamic tables are enabled but the blocked streams limit is not explicitly configured. A bug in limit checking and a memory leak in stream tracking allow unauthenticated remote attackers to exhaust the JVM heap memory and crash the server.
CVE-2026-50009 is a cryptographic design vulnerability in the Netty network application framework. Prior to version 4.2.15.Final, the framework's QUIC protocol implementation fails to cryptographically segregate the generated Connection IDs and the associated Stateless Reset Tokens. An on-path network attacker who sniffs traffic during a Connection ID rotation can extract secret token material from cleartext headers, enabling them to inject spoofed reset packets and terminate active connections.
A critical hostname verification bypass vulnerability exists in the Netty network application framework when configured as a TLS client. When a developer registers a custom plain X509TrustManager, Netty wraps it inside an X509TrustManagerWrapper to adapt it to the X509ExtendedTrustManager API. However, this wrapper discards the SSLEngine context, bypassing critical hostname checks. Because the wrapper is identified as an X509ExtendedTrustManager, standard cryptographic engines and Netty's OpenSSL wrappers do not re-wrap it, failing to execute any hostname validation. Consequently, clients silently accept certificates for any host, enabling unauthenticated Man-in-the-Middle (MitM) attacks.
An uncontrolled resource pre-allocation flaw in the Netty Redis codec module allows remote unauthenticated attackers to cause a denial of service (OutOfMemoryError) by sending a crafted Redis Serialization Protocol (RESP) array header.
CVE-2026-50020 is a medium-severity HTTP Request Smuggling/Response Smuggling vulnerability (CWE-444) within the Netty asynchronous network application framework. The flaw resides in Netty's HTTP codec implementation, specifically the HttpObjectDecoder class, which silently consumes arbitrary ISO control bytes preceding the first request line.