Sep 28, 2026·7 min read·7 visits
A shared-state vulnerability in Nodemailer's global DNS cache allows attackers to poison connection-specific TLS metadata. This redirects victim SMTPS sessions to an attacker-controlled virtual host, disclosing plaintext SMTP credentials.
Nodemailer versions 5.0.0 up to 10.0.1 are vulnerable to process-global state contamination inside the DNS caching subsystem. When SMTPS connections are established in a multi-tenant Node.js process targeting a shared gateway, a lower-privilege attacker can seed the global DNS cache with a malicious TLS servername. When a victim subsequently resolves the same gateway host, Nodemailer retrieves the polluted servername, overwrites the victim's connection settings, redirects the TLS session to the attacker's virtual host, and transmits the victim's cleartext SMTP credentials directly to the attacker.
Nodemailer is a widely adopted Node.js module designed for email transmission. To optimize performance and reduce latency during SMTP handshakes, the library implements an internal, process-global DNS cache (dnsCache). This cache mitigates the overhead associated with repeated resolution of target SMTP hosts.
In long-running Node.js processes, such as server-side SaaS platforms or centralized microservices, multiple isolated mail transports often run within a single execution space. If these transports route emails through a shared SMTP relay, they query the same hostname but configure distinct TLS options tailored to their respective domains. This configuration introduces a shared-state attack surface.
The vulnerability designated as GHSA-6VJ9-MWQ6-2F5V arises from an architectural failure to separate concerns within this global DNS cache. Specifically, connection-specific TLS configuration options are stored alongside network-specific DNS records. When multiple independent tenants utilize the shared cache, this design flaw allows a low-privilege attacker to contaminate the cache, leading to cross-tenant connection hijacking and credential disclosure.
The root cause of GHSA-6VJ9-MWQ6-2F5V is the storage of connection-specific TLS metadata within a process-global cache keyed only by the target hostname. Nodemailer maintains this cache inside src/shared/index.ts using a standard JavaScript Map object named dnsCache. This cache maps host string keys directly to DnsCacheValue structures.
The structural vulnerability lies in the definition of the DnsCacheValue interface, which encapsulates the TLS servername option. When a transport resolves a hostname using resolveHostname(), Nodemailer retrieves the dynamic TLS options provided by that specific connection attempt. If a cache miss occurs, the library populates the global dnsCache with the resolved IP addresses and binds the connection-specific servername to that entry.
const value: DnsCacheValue = {
addresses: allAddresses,
servername: options.servername || host
};Upon subsequent connection attempts to the same host within the five-minute Time-To-Live (TTL), Nodemailer triggers a cache hit. The formatDNSValue() utility extracts the cached value, copying the stale, tenant-specific servername directly into the returned lookup result.
const formatDNSValue = (value: DnsCacheValue | undefined, extra?: Partial<ResolvedHostname>): ResolvedHostname | undefined => {
return Object.assign(
{
servername: value.servername,
host,
_addresses: addresses
},
extra || {}
);
}This behavior corrupts the target parameters for any subsequent connection using the same host. During connection initialization inside src/smtp-connection/index.ts, Nodemailer's _resolveAndConnect() loop iterates over the keys of the returned DNS resolution object and blindly overwrites the existing connection options.
for (const key of Object.keys(resolved!)) {
if (key.charAt(0) !== '_' && (resolved as { [key: string]: any })[key]) {
(opts as { [key: string]: any })[key] = (resolved as { [key: string]: any })[key];
}
}This routine replaces the correct, transport-specific opts.servername configuration of the second tenant with the stale, poisoned servername from the cache.
To remediate this architectural vulnerability, the developers decoupled connection-level metadata from the shared network cache. The security patch was applied in commit a6512dbcb3c6e7f2f70d3acccc5752defe3c61fe.
Below is the code modification in src/shared/index.ts illustrating the removal of the state-sharing property.
// Vulnerable structure definition
export interface DnsCacheValue {
addresses: string[];
- servername: string | false;
}
// Patched formatting helper
const formatDNSValue = (value: DnsCacheValue | undefined, extra?: Partial<ResolvedHostname>): ResolvedHostname | undefined => {
return Object.assign(
{
- servername: value.servername,
host,
_addresses: addresses
},
extra || {}
);
}The patched implementation dynamically computes the TLS servername at the invocation of resolveHostname() instead of serializing it into the global cache. This guarantees that each calling transport maintains its independent server name metadata.
const host = options.host;
+ // The TLS server name belongs to the connection asking, not to the host it resolves.
+ // The cache is shared by every transport of the process and keyed by host alone.
+ const servername = options.servername || host;When returning cached values, the dynamic, connection-specific servername is passed back on-the-fly to prevent any cross-tenant leakage.
return callback(
null,
formatDNSValue(cached.value, {
+ servername,
cached: true
})
);This fix is architecturally complete. By purging the servername attribute entirely from the stored cache schema, the library eliminates the attack vector. There is no residual shared-state channel remaining in the DNS resolution path that can leak connection-specific TLS configuration details.
Exploitation of GHSA-6VJ9-MWQ6-2F5V requires a multi-tenant Node.js process where separate users configure distinct SMTPS transports targeting a shared gateway. The attack progresses in five logical phases: cache priming, victim lookup, connection routing, certificate validation, and credential extraction.
First, the attacker creates an SMTPS transport pointing to the target relay (e.g., smtp.sharedrelay.test) and sets the tls.servername property to an attacker-controlled domain (e.g., attacker.test). The attacker triggers a connection, forcing Nodemailer to resolve the host and write { addresses, servername: 'attacker.test' } into the global dnsCache.
Second, a victim tenant initiates a connection to the same relay using their legitimate credentials and expected server name (victim.test). Due to the global scope of dnsCache, Nodemailer retrieves the entry primed by the attacker and overwrites the victim's connection options, setting opts.servername to attacker.test.
Third, the victim's transport invokes the native tls.connect(opts) function. This transmits a TLS Client Hello containing the Server Name Indication (SNI) extension configured for attacker.test. The shared gateway parses this SNI and routes the TLS connection directly to the attacker's virtual server.
Fourth, the attacker's server completes the TLS handshake by presenting a valid certificate for attacker.test. Because the victim's validation options were overwritten to expect attacker.test, Node's native validation subsystem successfully validates the certificate chain against the trusted system root authorities.
Fifth, once the secure channel is established, the victim's Nodemailer client initiates the SMTP protocol flow. The client sends an EHLO command, reads the AUTH PLAIN capability from the attacker's server, and transmits its plain SMTP authentication credentials directly across the hijacked session.
The security impact of GHSA-6VJ9-MWQ6-2F5V is classified as high confidentiality loss. An attacker who successfully exploits this state pollution can acquire cleartext authentication credentials for other tenants operating within the same Node.js execution environment.
The Common Vulnerability Scoring System (CVSS) v3.1 vector is CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N, yielding a score of 5.9 (Medium). Although the impact on confidentiality is high, the overall rating is moderated by the high complexity of the attack. Exploitation depends on precise timing within the DNS TTL window, shared infrastructure routing via SNI, and co-location of transports in a single process.
Despite the Medium rating, this vulnerability represents a severe threat to SaaS applications, shared email routing infrastructure, and multi-tenant microservices. If credentials for an enterprise mail relay are compromised, the attacker can leverage them to bypass spam filters, send malicious mail from legitimate domains, and access sensitive corporate communications.
Security teams should immediately audit internal Node.js applications to identify instances of the nodemailer package. Applications with dependency versions between 5.0.0 and 10.0.1 must be considered vulnerable if they establish concurrent SMTPS connections to shared hosts.
The primary remediation strategy is upgrading the project's dependency structure to nodemailer version 10.0.2 or higher. This release resolves the underlying caching logic and ensures complete separation of connection-level metadata.
If immediate upgrading is not feasible, organizations can implement several mitigation workarounds. First, developers can bypass the global DNS resolver by using the direct IP address of the SMTP gateway (e.g., 192.0.2.1) in the host parameter, preventing Nodemailer from querying or writing to the global cache. Second, deploying separate Node.js processes for each tenant isolates the memory space and prevents cross-tenant state pollution.
Additionally, monitoring network logs for anomalies in SNI headers—specifically instances where the target IP matches a legitimate SMTP server but the SNI header contains an unrecognized or third-party domain—can help detect ongoing exploitation.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
nodemailer Nodemailer | >= 5.0.0, < 10.0.2 | 10.0.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-295 |
| Attack Vector | Network |
| CVSS | 5.9 (Medium) |
| Impact | High (Cleartext Credential Disclosure) |
| Exploit Status | PoC Available |
| KEV Status | Not Listed |
The product does not validate or incorrectly validates a certificate, which can allow an attacker to spoof a trusted entity by interfering with the connection path.
An uncontrolled resource consumption vulnerability exists in the multer middleware for Node.js (versions 2.2.0 through 2.3.0) when handling aborted multipart uploads using disk storage. Due to an asynchronous race condition in path resolution, files can become permanently orphaned on disk, leading to storage exhaustion and denial of service.
A high-severity Denial of Service (DoS) vulnerability exists in the undici WebSocket client implementation when processing compressed frames. The vulnerability is caused by a race condition where event listeners, including error handlers, are stripped from the active zlib stream during cleanup before the stream is fully terminated, leading to an unhandled exception.
An incomplete denylist vulnerability in FasterXML jackson-databind's DefaultBaseTypeLimitingValidator allows unauthenticated remote attackers to bypass polymorphic type limitations. By declaring properties of type java.lang.Comparable, attackers can instantiate arbitrary Comparable subclasses on the classpath, leading to path traversal, local file access, or application-specific state manipulation.
An authorization bypass vulnerability exists in the @grpc/grpc-js Node.js package (specifically within the xDS plugin wrapper @grpc/grpc-js-xds) due to a logical error in its Role-Based Access Control (RBAC) path matching component. When case-insensitive path matching is enabled, the matching logic performs a prefix comparison using the startsWith method instead of a strict equality comparison. This logic flaw allows unauthenticated or low-privilege clients with access to a shorter path to gain unauthorized access to longer, more privileged method names that share the same prefix.
A vulnerability in the scim-patch library allows authenticated users to pollute the global JavaScript execution environment. By transmitting a SCIM PATCH operation targeting inherited built-in methods, such as toString, valueOf, or hasOwnProperty, attackers bypass blocklist filters and mutate global prototype objects. This flaw occurs due to the library relying on standard prototype lookup and the 'in' operator during path-resolution and assignment, resolving to shared native functions instead of treating them as missing own-properties.
An OS command injection vulnerability in the grep_search tool of the code-ollama package allows remote code execution. This vulnerability is triggered when a local client executes the CLI against a malicious or compromised Ollama server. Due to grep_search being classified as a read-only tool, the CLI executes it automatically in Plan mode without human-in-the-loop validation, leading to zero-interaction local system compromise.