GHSA-2x7j-588g-ccc2
Nodemailer: Quadratic (O(n²)) time complexity in addressparser allows remote denial of service via a crafted address list
Summary
### 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); // 100000 addresses: ~6068 ms parsedAddresses.push.apply(parsedAddresses, handled); -> 100000 addresses: ~51 ms (≈119x faster, now linear) ``` **Measured scaling** (nodemailer 9.0.6, `'[email protected],'.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: })` 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', '[email protected],'.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 `[email protected]`. 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 = '[email protected],'.repeat(n); // n valid, comma-separated recipients const t0 = process.hrtime.bigint(); addressparser(payload); // blocks synchronously const ms = Number(process.
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| npm | nodemailer | — | 9.1.0 |
Remediation: Upgrade to 9.1.0 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.