Repository navigation
fix(deps): update dependency nodemailer to v10.1.4 [security] - #5027
Closed
renovate[bot] wants to merge 1 commit into
Closed
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
October 24, 2026 01:06
4b7fa88 to
13619ac
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
October 30, 2026 10:46
13619ac to
529d09d
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
November 5, 2026 17:02
529d09d to
2a12b80
Compare
Contributor
Contributor
Author
Renovate Ignore NotificationBecause you closed this PR without merging, Renovate will ignore this update ( If you accidentally closed this PR, or if you changed your mind: rename this PR to get a fresh replacement PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
10.0.9→10.1.4Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
Nodemailer: Quadratic (O(n^2)) DKIM header unfolding lets a large folded header cause remote denial of service during signing
CVE-2026-110871 / GHSA-39m8-27wv-hr27
More information
Details
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 intolinesand iterates backward; for each line matching the continuation pattern/^[ \t]/, it doeslines[i - 1] += '\n' + lines[i]andlines.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 bycheckHeaderswithout any size or line-count cap; the DKIM signer'sMAX_MESSAGE_SIZEandcacheTresholdknobs gate the body stream only, not the header block, so nothing bounds the number of continuation lines fed to the loop.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
parseHeadersunfolds the block. A minimal microbenchmark of the exact unfold logic demonstrates the complexity:Attack Chain
checkHeadersbuffers the whole header block intorawHeadersand callsparseHeaders(), the single sink, on every signed message.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:
checkHeadersaccumulates 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 backwardsplicemakes 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: npmPackage: nodemailerConfirmed affected range: <= 10.0.9Latest 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
splicewith 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).
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: Quadratic (O(n^2)) SMTP client reply parsing lets a malicious server or MITM cause a pre-auth remote denial of service
CVE-2026-110872 / GHSA-4g23-2xm8-66gc
More information
Details
Summary
The SMTP client response parser reassembles multiline server replies by appending each continuation line onto a single response-queue entry and re-testing whether that entry is still a partial (continuation) reply on every line. The partial-response test runs
str.slice(str.lastIndexOf('\n') + 1)on the growing entry, which forces a full flatten of the entire accumulated string on each line. A malicious or compromised SMTP server (or an on-path attacker on a plaintext or pre-STARTTLS hop) that streams continuation lines and never sends a completion line drives O(n^2) CPU on the single-threaded Node.js event loop before any authentication, freezing the whole process for an attacker-controlled duration.Root Cause
The invariant that a server cannot cause superlinear client-side work while a reply is being reassembled is violated. In
src/smtp-connection/index.ts,_onDatasplits inbound bytes into lines and, while the last queued entry is a continuation reply, concatenates each new line onto that same entry withthis._responseQueue[len - 1] += '\n' + lines[i]. On every iteration it callsisPartialResponse(lastline), which evaluates/^\d+-/.test(str.slice(str.lastIndexOf('\n') + 1)). Because the queued entry grows by one line per continuation line and is stored as a V8 rope (ConsString), eachlastIndexOf/sliceforces a full string flatten of length proportional to the accumulated reply. Executed once per line over n continuation lines, this is O(n^2) CPU. While the reply stays partial the function early-returns and never resets the entry, so the growth and the per-line reflatten continue unbounded. There is no response-size or line-count cap in the connection code; the only limits are time-based (greetingTimeout, and an inactivitysocketTimeout), and neither bounds accumulated reply size.Impact
An attacker who is, or can impersonate, the SMTP peer the client connects to can freeze the connecting Node.js process. Because Node.js is single-threaded, event-loop starvation halts all other work in that process (every other connection, request, and timer), not just the offending SMTP session. The reply is parsed during the greeting and EHLO phases, before the client authenticates or issues any command, so no credentials on the client are required. The blocking duration scales with the volume of continuation data the attacker streams and can be sustained indefinitely by continuing to stream, and memory for the accumulated entry also grows without bound. Independent reproduction on Node.js 24 measured roughly 11.6 seconds of event-loop blocking for about 640 KB of continuation data and roughly 47 seconds for about 1.3 MB, scaling quadratically. This is a repeatable remote denial of service against the sending process.
Proof of Concept
Node.js 24; a raw TCP listener acting as a hostile SMTP server; the client uses the shipped SMTPConnection (require('nodemailer/lib/smtp-connection')).Steps: (1) start a TCP server that accepts a connection and streams continuation lines of the form
220-flood...\r\nin large batches, never emitting a completion line such as220 ready; (2) point an SMTPConnection at it and observe the client event loop stall while the single response-queue entry grows and is reflattened on every line. A minimal microbenchmark of the exact parsing logic demonstrates the complexity:Attack Chain
datahandler_onData, the sole data handler; every reply phase traverses the same continuation-append loop._onDataruns synchronously; the inactivity timer resets on every inbound chunk, so an actively streaming attacker never trips it.isPartialResponse, yielding O(n^2) synchronous CPU on the event loop.Bypass Evidence
The relevant guard would be a cap on accumulated reply size or line count. A search of the connection module (
maxSize,maxLength,responseQueue.length >, byte or line ceilings) finds none; the only bounds aregreetingTimeoutand the inactivity-basedsocketTimeout, both time-based and neither bounding accumulated size. Disproof attempts: the greeting timeout does not help because it is time-based, fires only during the greeting phase, is cleared once the greeting completes, and its callback is delayed while_onDataruns synchronously; a well-behaved server does not trigger the issue because a completion line resets the entry and keeps cost linear, but the modeled attacker is a hostile peer; segmented delivery does not help the defender because the response queue and line remainder persist across_onDatacalls so the accumulation and quadratic cost survive chunking; there is no safer alternate caller because_onDatais the only data handler and the append loop is on every reply phase. Note on scope: core nodemailer connects to a configured host and does not perform MX resolution, so this is not reachable through a direct-to-MX transport; the malicious-peer, multi-tenant tenant-supplied-host, deliverability-probe, and MITM paths establish reachability.Affected Versions
Ecosystem: npmPackage: nodemailerConfirmed affected range: <= 10.0.9Latest 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: 10.0.10The quadratic reassembly shape is long-standing and predates the addressparser 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 that reassembling a multiline reply is linear in the reply size and that the reply size is bounded. Track the running length of the accumulated entry and the continuation state without re-scanning the whole entry on every line (for example, test the newly arrived line rather than slicing the tail of the growing accumulator, or collect lines in an array and join once at completion). Independently, impose a configurable maximum on the accumulated reply size or continuation-line count and fail the connection when it is exceeded, so a peer that never completes a reply cannot force unbounded work or memory. The remediation must cover every reply phase (greeting, EHLO, and command responses), because all of them flow through the same handler. As an interim mitigation, only connect to trusted SMTP servers over authenticated TLS (avoiding plaintext or STARTTLS-strippable hops), and in multi-tenant deployments isolate outbound SMTP sessions to tenant-supplied hosts so one tenant cannot starve a shared process.
Reported by zx (GitHub: @manus-pi).
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: Quadratic backtracking in SMTP AUTH mechanism detection regexes allows pre-auth remote denial of service
CVE-2026-110869 / GHSA-4ffr-jq9g-5ffx
More information
Details
Summary
Nodemailer's SMTP client applies four regular expressions to a peer's EHLO response to detect the supported SASL mechanisms. Each pattern uses an alternation with two overlapping whitespace-matching sub-patterns, which produces quadratic (O(n^2)) backtracking whenever the mechanism name is absent from the line. A malicious or MITM SMTP server that answers EHLO with
250-AUTHfollowed by a long whitespace-only run and a non-matching token blocks the Node.js event loop for tens of seconds to hours during a pre-authentication handshake. No authentication to the target SMTP server is required, and any application that connects to an attacker-influenced host is affected.Root Cause
Each of the four AUTH mechanism regexes in
SMTPConnection._actionEHLOcontains the sub-pattern(?:(\s+|=)[^\n]*\s+|\s+|=)before the mechanism literal.[^\n]*and the trailing\s+both match whitespace and can consume the same characters, so when the mechanism literal is absent, the regex engine must try every partition of the whitespace run between[^\n]*and\s+before failing. The four regexes run unconditionally on every EHLO response, with no length cap, no shape validation, and no early rejection before the engine enters the ambiguous alternation.Impact
Algorithmic-complexity denial of service against any Node.js process that opens an SMTP connection to an attacker-controlled or attacker-influenced host. Node.js is single threaded, so the process stalls entirely while the regex engine backtracks: no other requests are served, no timers fire, and the built-in socket timeout (10 minutes) cannot interrupt synchronous JavaScript. A 4,000-character whitespace run inside a single continuation line already blocks each regex for tens of seconds; the client bounds a response at 1 MB, which leaves headroom for blocking runs measured in hours across the four patterns. Two realistic attacker positions reach the sink pre-authentication: a hosted mailer that lets tenants configure their own SMTP server, and any network-adjacent attacker able to man-in-the-middle a cleartext SMTP connection (including before STARTTLS negotiation).
Proof of Concept
The PoC exercises the exact four regexes copied from
src/smtp-connection/index.tsagainst a synthesized250-AUTH<whitespace>NOMATCHline, so no live SMTP server is needed to reproduce the timing.Doubling the whitespace run multiplies the wall time by roughly four, which matches the expected O(n^2) growth. The well-formed control shows that a normal EHLO response is parsed in effectively zero time.
Attack Chain
nodemailerto send mail, opens a TCP or TLS connection to an SMTP host the attacker controls. This happens automatically for multi-tenant mailers whose tenants configure their own outbound SMTP server, and for any client whose plaintext SMTP session can be intercepted by a network-adjacent attacker before STARTTLS negotiation.EHLO. In particular, the attacker chooses the length and content of every250-continuation line up to the client's 1 MB response cap._onData, decoded by_processResponse, and dispatched to_actionEHLO. On the first EHLO after the greeting (malicious server with no STARTTLS), on the second EHLO after a successful STARTTLS upgrade, or on the first EHLO whenignoreTLSis set, control reaches the AUTH detection block near the end of_actionEHLO.maxResponseSize(1 MB by default) and the 421 or non-2xx status-code check. Neither prevents O(n^2) backtracking: 4,000 whitespace characters already block each regex for tens of seconds, well below the 1 MB cap. No length, shape, or sanitization check runs before the regexes.RegExp.prototype.testcalls execute against the crafted line, each entering catastrophic backtracking because[^\n]*and\s+share the whitespace alphabet and the mechanism literal never matches.Bypass Evidence
The only response-side protection in this code path is
maxResponseSize, introduced by the fix for GHSA-4g23-2xm8-66gc (commitf2d82fa). That fix targeted quadratic reassembly of long multiline replies inside_onDataand left the AUTH regex evaluation in_actionEHLOuntouched.maxResponseSizedoes not prevent this issue: at 4,000 whitespace characters, roughly 0.4 percent of the default 1 MB cap, a single continuation line already blocks each of the four regexes for tens of seconds. No status-code or length gate rejects the payload before the regex engine enters the ambiguous alternation, and Node.js's single-threaded event loop cannot service the socket timeout while a synchronoustest()call is running. Two other recent linear-time fixes in this project (the addressparser fallback for GHSA-v53p-9fqp-m79j and the DKIM header unfolding for GHSA-39m8-27wv-hr27) address entirely different modules and inputs and do not touch the AUTH mechanism regexes.Affected Versions
npmnodemailer>= 3.0.0, <= 10.0.1210.0.12(npm registry)The four AUTH mechanism regexes were introduced when the SMTP client was bundled into
nodemailerat 3.0.0 (previously a separatesmtp-connectionpackage). The vulnerable alternation(?:(\s+|=)[^\n]*\s+|\s+|=)is present, unchanged, in every published release from 3.0.0 (lib/smtp-connection/index.js) through 10.0.12 (dist/esm/smtp-connection/index.jsanddist/cjs/smtp-connection/index.js). A later refactor added a leading[ -]character class but did not alter the ambiguous inner alternation. No release, and no development branch inspected at the current HEAD (30e4cfd), contains a fix for this pattern.Suggested Fix
Replace the four backtracking regexes with a non-ambiguous parse of the AUTH extension. The invariant the fix must enforce is that mechanism detection runs in time linear in the response length regardless of whitespace shape. A minimal approach: match the AUTH extension value with a single non-backtracking pattern (for example,
/[ -]AUTH(?:\s+|=)([^\r\n]*)/i) and then check for each mechanism name against the whitespace-split tokens with aSetlookup orincludes. The fix must cover both continuation forms (250-AUTH ...and250 AUTH ...) and both delimiters (space and=), and must handle the "AUTH advertised with no mechanisms" case without walking the whitespace run. As an interim mitigation, callers who never connect to an untrusted SMTP peer can restrict SMTP endpoints to a known allowlist; this does not help clients that must interoperate with arbitrary user-configured servers or with any cleartext SMTP path exposed to a network-adjacent attacker before STARTTLS.Reported by zx (GitHub: @manus-pi).
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: RFC 5322 comment inside an angle-bracket address is never stripped
CVE-2026-110870 / GHSA-g73g-hqqh-jr95
More information
Details
Summary
Nodemailer's address parser has three published, patched fixes for RFC 5322 comment
( ... )handling inside an addr-spec: GHSA-cc9r-2j5m-2m83 (comment glued two domain atoms into one), GHSA-g57g-f23g-4646 (the same glue for a quoted local part), and the related ReDoS fixes GHSA-prgh-xp8r-p3m5 / GHSA-v53p-9fqp-m79j. All three fixes live in_handleAddress()'sjoins/_recoverAddrSpec()logic insrc/addressparser/index.ts, and that logic is exercised whenever the local text-accumulation path in the tokenizer runs.That logic is never reached at all for the single most common way a recipient is written: a display name followed by an angle-bracket address,
Name <local@domain>. When the input inside the angle brackets itself contains an RFC 5322 comment, e.g.the comment is not recognized as an operator, is not stripped, and the parenthesis characters survive verbatim inside the parsed address. That value is used as-is for
envelope.toand is written raw onto the wire in theRCPT TOSMTP command. I confirmed this end to end:jsonTransport/streamTransport(envelope.toand the emittedTo:header both carry the malformed value), andaiosmtpd), where the malformed value is fatal to the transaction (501 Syntax: RCPT TO: <address> [SP <mail-parameters>], all recipients rejected).This reproduces unmodified on every version I tested from
6.9.16through the current10.0.12HEAD, i.e. it predates and survives all three of the address-parser advisories above unchanged, because it is a different root cause in a code path those fixes never touch.Root cause
1. The tokenizer has a single-slot operator stack
src/addressparser/index.ts,Tokenizer.checkChar():(currently around line 670)
operatorExpectingis a single string, not a stack. Once<opens the address scope,operatorExpectingis set to'>'. The branch above that recognizes a new operator character (such as() is gated on!this.operatorExpecting, which is now false. So while inside<...>, a(is never tokenized as an operator at all — it is just an ordinary character appended to the running text token, together with everything up to the next>. The RFC 5322 comment is not recognized, not entered, and not stripped: it rides straight through as literal text.2. The
_handleAddressstate machine loses theaddressstate on any operator boundarysrc/addressparser/index.ts, inside_handleAddress()(currently around line 365-388):Even where a comment is recognized (outside angle brackets), closing it with
)falls through todefaultand resetsstateto'text', not back to whatever state was active before the comment opened. This is why the fixed cases (bare address, quoted local part) only work at all: the leaked comment tail lands in the free-textbucket, where_recoverAddrSpec()/the ADDR_SPEC extraction logic can find and strip it. Inside<...>, none of that applies in the first place (point 1), so there is no text-bucket safety net to fall back on — the whole point 1/point 2 interaction is what makes the angle-bracket case categorically different from, and unprotected by, the three existing fixes.3. The one existing safety net requires whitespace, and this value has none
_recoverAddrSpec()(currently around line 283) is the function all three prior fixes rely on to peel a clean addr-spec back out of a polluted value. Its very first line is:user@good-corp.com(x)evil.comcontains no whitespace at all — the comment contentxhad none, and nothing in the angle-bracket path inserts any — so_recoverAddrSpecbails out immediately and never even looks at the value. There is no cleanup path for this shape.Proof of Concept
Environment: Node.js, current
master(commit30e4cfd5902fd5da4d1bc76dc8e4bc92d288adef, package.json version10.0.12), no special transport configuration required.Parser-level confirmation
Compare with the already-fixed, unbracketed form of the exact same bytes:
sendMail/ envelope confirmation (jsonTransport, no network)Observed output:
The raw
streamTransportmessage shows the same value in the emitted header:Wire-level confirmation against a real SMTP server
Observed:
The same recipient string sent at a real, conformant SMTP server (
aiosmtpd, a widely used reference-grade Python SMTP implementation) instead of the sink above:So against a strict receiver the immediate, concretely demonstrated outcome is that the message is bounced: an ordinary-looking recipient address that a sender/application believes is valid (and, see below, that some app-side validators do read as valid, at
good-corp.com) silently fails to send.Interpretation-conflict confirmation (same class as GHSA-cc9r-2j5m-2m83)
The same string, read by two other parsers an application might use to gate on the recipient's domain, produces two more, mutually different readings:
So for the exact same bytes:
email.utils.parseaddrgood-corp.comevil.com(attacker-registrable)email.policy.default(defects not inspected)good-corp.com(clean, would pass an allow-list check)addressparser(this report)good-corp.com(x)evil.com(illegal characters, rejected by a strict MTA)This is the exact "which parser sees what" comparison table from GHSA-cc9r-2j5m-2m83, reproduced through a different, unpatched code path: an application validating with
email.policy.defaultwithout checking.defects(a documented pattern in that advisory) approvesgood-corp.comand hands the string to Nodemailer, which then emits a different, malformed value on the wire.Version range
The built
addressparser/sendMailoutput was tested for this exact input across published releases:envelope.to6.9.16user@good-corp.com(x)evil.com9.0.6user@good-corp.com(x)evil.com9.1.0(fixed GHSA-cc9r-2j5m-2m83)user@good-corp.com(x)evil.com10.0.1user@good-corp.com(x)evil.com10.0.12/ current HEAD (fixed GHSA-g57g-f23g-4646)user@good-corp.com(x)evil.comUnchanged across every one of the three address-parser advisory fixes, because none of them touch the tokenizer's single-slot
operatorExpectingstate or the_handleAddressstate-machine reset on).Impact
To/Cc/Bcc/From/Reply-Tovalue is written (Display Name <user@domain>) — is parsed into a value containing SMTP-illegal characters ((,)) that a conformant receiving SMTP server rejects outright, silently failing message delivery to what the sender/application believed was a valid, already-validated recipient (CWE-20, availability/reliability impact on outbound mail).envelope.toand the wireRCPT TOcommand disagrees with how other parsers, including ones explicitly called out as an at-risk validation pattern in GHSA-cc9r-2j5m-2m83 (Python'semail.policy.defaultwithout defect inspection), read the exact same bytes. An application gating outbound mail on "is the recipient's domain in my allow-list" using such a validator can approve a value that Nodemailer then sends corrupted, and depending on the receiving MTA's own comment handling, a different domain than either the sender or the validator believed was being used.MimeNode._parseAddresses()—to,cc,bcc,from,replyTo, and the explicitenvelopeoption — is affected identically, since they all funnel through the sameaddressparser()/_handleAddress()logic.Suggested fix
The underlying issue is architectural:
Tokenizer.operatorExpectingis a single value, so it cannot represent "currently inside an angle-address, and also currently inside a comment nested within it." A minimal fix is to make the tokenizer maintain a stack of expected closing characters (pushing')'when entering a comment while another operator scope, e.g.'>', is already open, and popping back to the outer expectation on)), so a comment can be recognized and stripped no matter what scope it is nested inside._handleAddress's state machine would then need to restore the previousstateon a comment close instead of unconditionally resetting to'text', so recognized-but-stripped comment content inside<...>continues to accumulate asaddressrather than leaking intotext.As defense in depth,
_recoverAddrSpec()(or an equivalent check run specifically ondata.addressfor the angle-bracket case) should also reject/clean any parsed address value that still contains RFC 5322 special characters such as(/)regardless of whether whitespace is present, sinceenvelope.tois used directly to build an SMTP command and any character outsideMailbox(RFC 5321 §4.1.2) reaching that string is already the wrong outcome, independent of how it got there.Regression test suggestion
Name <user@example.com(x)evil.com>and the bcc/cc/from/replyTo/envelope equivalents should all parse to{ address: 'user@example.com', name: 'Name evil.com' }or be rejected outright bysendMail, never silently carry(/)intoenvelope.to.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: A ">" inside a quoted local part ends an angle-addr early in addressparser, delivering to a domain the header does not name
CVE-2026-121426 / GHSA-85x2-q946-cp5h
More information
Details
Vulnerability summary
addressparser ends an angle-addr at the first
>it sees, including one inside a quoted local part, a comment or a domain literal. The domain is then read off the text in front of that>.<"a@evil.com>"@good.com>is the mailbox"a@evil.com>"@good.comunder RFC 5322, and Nodemailer read it as an address atevil.com. The unbracketed spelling of the same value was already read correctly. Affected: 3.0.0 through 10.1.3.Attacker capabilities
The attacker controls the text of a recipient or sender address that an application passes to Nodemailer, for example a To value entered in a contact or invite form, or the header of an inbound message parsed with mailparser. No access to the application, its configuration or its credentials is needed. The value is a syntactically valid RFC 5322 address that conformant parsers accept without defects.
Realistic exploitation scenario
An application lets users send invitations or notifications only to addresses at an allowed domain and validates the submitted value with a conformant parser or by checking its domain. The attacker submits
<"a@evil.com>"@good.com>, which validates as agood.commailbox. Nodemailer then parses the same value, issuesRCPT TO:<"\"a"@evil.com>and the message, with whatever content the application puts into it, is delivered to the attacker's domain. The security boundary crossed is the application's recipient allow list. In the inbound direction, mailparser reports a different address than a conformant reader of the same header, so a display or policy layer built on the two disagrees.Proof of concept
Python's
email._header_value_parser.get_address_listreads the same value as"a@evil.com>"@good.com, with no defects reported.The envelope a message gets is built from the same parse, so
sendMail({ to: "<\"a@evil.com>\"@good.com>" })issues the RCPT TO shown above.Security impact
Mail that an application intends for an allowed domain is delivered to a domain chosen by the attacker, which discloses its content. It is a security vulnerability rather than a parsing bug because the misreading only occurs for values that a conformant validator approves, so validation done the right way is bypassed. Affected are applications that pass user influenced addresses to Nodemailer and consumers of the standal