Mittel

Nodemailer: Quadratic (O(n^2)) DKIM header unfolding lets a large folded header cause remote denial of service during signing

  • Veröffentlicht: 22.10.2026
  • Aktualisiert: 22.10.2026 19:17

Summary

When nodemailer DKIM-signs an outbound message, its DKIM message parser unfolds RFC 5322 header continuation lines with a loop that concatenates each continuation line onto the preceding line and then re-tests the anchored regex /^[ \t]/ against the growing merged line on every iteration. Re-testing the regex against the ever-growing accumulated line forces a full string flatten proportional to the accumulated length on each iteration, yielding O(n^2) CPU in the number of continuation lines. An application user who influences the size of a header that folds into many continuation lines (most naturally a large recipient list, but also a long custom header, Subject, or References) can drive tens of thousands of continuation lines and block the single-threaded Node.js event loop for seconds during signing, a repeatable denial of service.

Root Cause

The invariant that signing a message performs work linear in the message size is violated in the header-unfolding step. In src/dkim/message-parser.ts, parseHeaders() splits the buffered header block into lines and iterates backward; for each line matching the continuation pattern /^[ \t]/, it does lines[i - 1] += '\n' + lines[i] and lines.splice(i, 1). The dominant cost is not the array splice: iterating backward keeps each splice near the shrinking tail, so it shifts only the few trailing header lines. The quadratic cost is the anchored regex test /^[ \t]/.test(lines[i]) (together with the += concatenation) evaluated against the growing merged line on every iteration. The += builds an O(1) rope, but the next iteration's regex test forces a full flatten of the entire accumulated line, so the total cost is the sum over all continuation lines, which is O(n^2). The header block is buffered by checkHeaders without any size or line-count cap; the DKIM signer's MAX_MESSAGE_SIZE and cacheTreshold knobs gate the body stream only, not the header block, so nothing bounds the number of continuation lines fed to the loop.

Affected file and function (verified at the published v10.0.9 artifact, in src/ and compiled verbatim into the published dist output):
- src/dkim/message-parser.ts: MessageParser.parseHeaders (the continuation-unfold loop that re-tests /^[ \t]/ against the growing merged line)

Impact

An attacker or user who can make an outbound, DKIM-signed message carry a header that folds into a large number of continuation lines can freeze the sending Node.js process during signing. Because Node.js is single-threaded, the synchronous unfold loop starves the entire event loop, halting all other work in the process, and it is repeatable on every such message. The natural lever is a large recipient list: nodemailer's own MIME composer folds a To or Cc value at column 76 into space-prefixed continuation lines, and the count scales linearly with the number of recipients, so a caller-supplied recipient list (common in bulk-mail, notification, and multi-tenant email APIs) turns into tens of thousands of continuation lines. The single precondition is that DKIM signing is enabled, which is a mainstream deliverability configuration. Independent reproduction on Node.js 24 measured roughly 2.8 seconds of event-loop blocking at about 20,000 continuation lines and roughly 11.5 seconds at about 40,000, scaling quadratically; the full composer-to-signer chain with a 60,000-recipient To header blocked for roughly 6.6 seconds. The applications that cap recipient count or header size shrink the exposed population but do not change the per-message availability impact once a large header is accepted.

Proof of Concept

Node.js 24; the shipped DKIM signer (require('nodemailer/lib/dkim')) or the exact unfold loop.

Steps: (1) construct a message whose header block contains one header value folded into many space-prefixed continuation lines (for example by supplying a very large recipient list, which nodemailer's MIME composer folds automatically); (2) DKIM-sign it and observe the event loop stall while parseHeaders unfolds the block. A minimal microbenchmark of the exact unfold logic demonstrates the complexity:

js
// Reproduces the parseHeaders unfold loop verbatim (Node 24).
function unfold(headerBlock) {
    const lines = headerBlock.split(/\r?\n/);
    for (let i = lines.length - 1; i > 0; i--) {
        if (/^[ \t]/.test(lines[i])) {
            lines[i - 1] += '\n' + lines[i];
            lines.splice(i, 1);
        }
    }
    return lines;
}
function run(n) {
    let block = 'To: a@b.c\r\n';
    for (let i = 0; i < n; i++) block += ' cont-' + i + '@example.com,\r\n';
    for (let k = 0; k < 5; k++) block += 'X-H' + k + ': v\r\n';
    const t = Date.now();
    unfold(block);
    return Date.now() - t;
}
for (const n of [5000, 10000, 20000, 40000]) console.log(n, run(n) + 'ms');
5000   27ms
10000  542ms
20000  2853ms
40000  11503ms
(2x input yields roughly 4x-5x time: quadratic. The cost is the regex re-flatten of the growing merged line, not the splice.)

Attack Chain

  1. Exposure: An application DKIM-signs outbound mail (a mainstream deliverability configuration, enabled per transport or per message) for a message whose header size is influenced by a user, for example a large recipient list, a long custom header, Subject, or References.
  2. Control: The attacker or user controls the size of that header. The MIME composer folds it deterministically at column 76 into space-prefixed continuation lines that match the continuation pattern, so the continuation-line count scales linearly with the supplied size.
  3. Path: Signing pipes the composed message through the DKIM MessageParser; checkHeaders buffers the whole header block into rawHeaders and calls parseHeaders(), the single sink, on every signed message.
  4. Guard: No header-size or line-count cap exists between the input and the sink; the signer's body-only size thresholds do not apply to the header block.
  5. Primitive: The unfold loop re-tests the anchored regex against the growing merged line on each continuation line, forcing a full flatten per iteration and yielding O(n^2) synchronous CPU.
  6. Result: Event-loop starvation of the single-threaded sending process for an attacker-scaled duration, repeatable per message: a denial of service.

Bypass Evidence

The relevant guard would be a cap on header-block size or continuation-line count before unfolding. A search of the DKIM module confirms none exists: checkHeaders accumulates header chunks unbounded until the header terminator, and the only size knobs (MAX_MESSAGE_SIZE, cacheTreshold) are checked against the body stream, not the header block. The strongest disproof attempt was that the backward splice makes the loop cheap for a single consecutive folded block (so it would not be quadratic); this was refuted by a mechanism-isolation benchmark: removing the splice entirely leaves the loop just as slow (about 11 seconds at 40,000 lines), while precomputing the continuation test on the original lines so the growing merged line is never re-tested collapses it to a few milliseconds. This proves the quadratic driver is the regex re-flatten of the accumulated line, not the array shift, and confirms the loop is quadratic even for a single consecutive folded block. No published advisory covers this file: the disclosed complexity issues all live in the address parser and recipient-normalization code, and the DKIM message parser has never received a complexity fix.

Affected Versions

  • Ecosystem: npm
  • Package: nodemailer
  • Confirmed affected range: <= 10.0.9
  • Latest release checked: nodemailer 10.0.9 (npm registry; vulnerable code verified at git tag v10.0.9 and in the published dist output)
  • Fix status: not fixed (no released version removes the quadratic unfold or caps header size)

The unfold loop is long-standing DKIM code that predates the address-parser complexity fix family, so the exact introducing release is not positively established and the lower bound is left open; the upper bound is verified against the current published artifact, which is vulnerable with no fix.

Suggested Fix

Enforce linear-time header unfolding and bound the header block. The key correction is to stop re-scanning the growing accumulated line: classify each line's continuation status on the original split lines before merging, and build the unfolded value with an array plus a single join rather than repeatedly re-testing and concatenating the growing string. Simply replacing splice with a filter or two-pointer compaction while still re-testing the merged line would not fix the issue, because the regex re-flatten of the accumulated line is the quadratic driver. Independently, impose a configurable maximum on the buffered header-block size or continuation-line count in the message parser and fail closed when it is exceeded, so an oversized folded header cannot force unbounded work. As an interim mitigation, applications should cap recipient counts and header-value sizes for messages that will be DKIM-signed.

Reported by zx (GitHub: @manus-pi).

Betroffene Software

Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.

PlattformPaketAnfällige VersionenBehoben in
npm-Paket nodemailer >= 3.0.0, < 10.0.10 10.0.10

Verweise

CVSS-Wert 6.5/10
CVSS-Vektor
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Schwäche (CWE)
CWE-400, CWE-407
Plattform
npm
Paket
nodemailer
Veröffentlicht
22.10.2026
Aktualisiert
22.10.2026 19:17

Quellen und Lizenzen

OSV / GitHub Advisory Database - GHSA-39m8-27wv-hr27 - CC BY 4.0

Schwachstellenhistorie: nodemailer

Alle veröffentlichten Schwachstellen dieses Pakets (npm-Paket), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.

Alle (21) im Archiv
Einträge insgesamt
21
Erste → letzte
05.2021 10.2026
Ohne Fix
0
In Angriffen ausgenutzt
0

Verteilung nach Schweregrad

  • Kritisch 1
  • Hoch 6
  • Mittel 13
  • Niedrig 1

Veröffentlichungen pro Jahr

  1. 2021: 2
  2. 2022: 0
  3. 2023: 0
  4. 2024: 1
  5. 2025: 2
  6. 2026: 16
Schweregrad:
  1. Nodemailer: Quadratic (O(n^2)) DKIM header unfolding lets a large folded header cause remote denial of service during signing dieser Eintrag
    Mittel · 6.5 CVE-2026-110871 >= 3.0.0, < 10.0.10 10.0.10
  2. Hoch · 7.5 CVE-2025-14874 >= 3.0.0, < 7.0.11 7.0.11
  3. Kritisch · 9.8 CVE-2020-7769 < 6.4.16 6.4.16

Erkennen Sie Schwachstellen in Ihrer Anwendung

Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.

Anwendung verbinden
DockRay Newsletter

Anwendungssicherheit in Ihrem Postfach

Wichtige Schwachstellen, konkrete Empfehlungen und praktische Tipps für Ihre Anwendungen. Wählen Sie Ihre Technologien und verfolgen Sie, was für Ihr Projekt relevant ist.

  • Eine tägliche Zusammenfassung neuer Schwachstellen, auch kritischer.
  • Benachrichtigungen über wichtige Updates.
  • Praktische Tipps zur Sicherheit und Überwachung von Anwendungen.

Leer lassen, um Meldungen zu allen Technologien zu erhalten.

Kostenlos anmelden. In jeder E-Mail mit einem Klick abmelden.