CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27492
4.7

Singleton Sins: Leaking Data via Lettermint's Fluent Interface

Alon Barad
Alon Barad
Software Engineer

Feb 21, 2026·5 min read·6 visits

PoC Available

Executive Summary (TL;DR)

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.

The Allure of the Fluent Interface

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.

The Dirty Table Problem

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.

Autopsy of the Code

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.

Crossing the Streams (Exploit)

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.

  1. Step 1 (The Setup): The attacker triggers an action that generates a heavy email. Perhaps they upload a profile picture, which the system emails back as an attachment.
  2. Step 2 (The Trigger): The attacker immediately triggers a second, lighter action, like a "Forgot Password" request.
  3. Step 3 (The Capture): If the backend reuses the SDK instance, the "Forgot Password" email will arrive containing the attachment from Step 1.

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.

The Race Condition That Remains

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:

  1. Request A calls sdk.to('admin').
  2. Request A awaits a database query.
  3. Request B calls sdk.to('hacker') on the same instance.
  4. Request A resumes and calls 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.

Official Patches

LettermintOfficial Release v1.5.1

Fix Analysis (2)

Technical Appendix

CVSS Score
4.7/ 10
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N

Affected Systems

Node.js Applications using lettermint-node <= 1.5.0Lettermint Email SDK Integrations

Affected Versions Detail

Product
Affected Versions
Fixed Version
lettermint-node
Lettermint
<= 1.5.01.5.1
AttributeDetail
CWECWE-488 (Data Element to Wrong Session)
CVSS4.7 (Medium)
Attack VectorLocal (Context Dependent)
Exploit MaturityPoC Available
AuthenticationSingle Instance Reuse Required
ImpactData Leakage / PII Exposure

MITRE ATT&CK Mapping

T1213Data from Information Repositories
Collection
T1592Gather Victim Host Information
Reconnaissance
CWE-488
Exposure of Data Element to Wrong Session

Exposure of Data Element to Wrong Session

Known Exploits & Detection

GitHubFunctional PoC included in the unit tests of the patch.

Vulnerability Timeline

Vulnerability identified and patched
2026-02-20
CVE-2026-27492 Published
2026-02-21

References & Sources

  • [1]GHSA Advisory
  • [2]CWE-488 Definition

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.