fix(deps): update dependency nodemailer to v9 [security] - #164
Open
renovate[bot] wants to merge 1 commit into
Open
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
June 18, 2026 20:57
0f496c7 to
3b9d33f
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
July 12, 2026 12:39
3b9d33f to
fb53146
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
August 12, 2026 00:37
fb53146 to
fce727e
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
August 26, 2026 20:57
fce727e to
93992ad
Compare
renovate
Bot
force-pushed
the
renovate/npm-nodemailer-vulnerability
branch
from
September 2, 2026 19:33
93992ad to
632d514
Compare
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.
This PR contains the following updates:
^8.0.0→^9.0.0Nodemailer: Improper TLS Certificate Validation in OAuth2 Token Fetch Enables Credential Interception
GHSA-r7g4-qg5f-qqm2
More information
Details
Summary
Nodemailer disables TLS certificate verification in its internal HTTPS fetch client through the use of rejectUnauthorized: false inside lib/fetch/index.js.
As a result, OAuth2 token requests trust invalid or self-signed HTTPS certificates and transmit sensitive OAuth credentials over connections that should fail TLS validation.
An attacker in a machine-in-the-middle position can intercept OAuth2 credential exchanges and capture:
The issue was verified through runtime testing using a self-signed HTTPS OAuth endpoint.
Details
Root Cause
The issue originates from the internal HTTPS fetch implementation used by Nodemailer for OAuth2 token retrieval and related outbound HTTPS requests.
Inside:
lib/fetch/index.jsthe request options contain:
rejectUnauthorized: falseThis disables TLS peer certificate verification globally for the internal HTTPS client unless explicitly overridden through optional TLS configuration.
As a result:
This violates expected HTTPS security guarantees.
Vulnerable Flow
The vulnerable execution chain is:
OAuth2 Transport
↓
XOAuth2 token generation
↓
Internal HTTPS fetch client
↓
HTTPS request with rejectUnauthorized:false
↓
Attacker-controlled/self-signed endpoint trusted
↓
OAuth credentials transmitted
PoC
Environment
Mail API (app/server.js)
Malicious HTTPS OAuth Server (poc/evil-oauth.js)
Nodemailer OAuth2 Test (test.js)
Steps to Reproduce
PIC

Impact
The issue effectively downgrades HTTPS security protections for sensitive OAuth credential exchanges.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer jsonTransport bypasses disableFileAccess and disableUrlAccess during message normalization
GHSA-wqvq-jvpq-h66f
More information
Details
Summary
Nodemailer's
disableFileAccessanddisableUrlAccessoptions are intended to prevent message content and attachments from reading local files or fetching URLs. The normal MIME streaming path enforces those options inMimeNode._getStream(). However,jsonTransportserializes messages by callingmail.normalize(), which resolveshtml,text, alternatives, calendar events, and attachments throughshared.resolveContent()before MIME generation.shared.resolveContent()reads local files and fetches HTTP(S) URLs directly, without receiving or checkingdisableFileAccessordisableUrlAccess.As a result, applications that use
jsonTransportas a safe serializer or queue payload generator while relying ondisableFileAccess/disableUrlAccesscan still be made to read local files into the generated JSON output or make outbound HTTP requests when an attacker controls message content fields such as attachmentpathortext.href.The same missing-enforcement root cause is also reachable before normal streaming when
attachDataUrlscauses_convertDataImages()to callmail.resolveContent(mail.data, 'html', ...); this should be fixed with the same access-control check.Details
Source-to-sink evidence:
lib/nodemailer.js:42-45selectsJSONTransportwhencreateTransport({ jsonTransport: true, ... })is used.lib/mailer/mail-message.js:34-39copies transport-leveldisableFileAccessanddisableUrlAccessoptions intomail.data.lib/json-transport/index.js:52-76serializes mail by callingmail.normalize((err, data) => ...).lib/mailer/mail-message.js:46-135implementsresolveAll()and callsshared.resolveContent(...args, ...)forhtml,text,watchHtml,amp,icalEvent, alternatives, and attachments.lib/shared/index.js:506-562implementsresolveContent().lib/shared/index.js:540-541fetches HTTP(S) content withnmfetch(content.path || content.href).lib/shared/index.js:549-550reads local files withfs.createReadStream(content.path).shared.resolveContent()does not checkdisableFileAccessordisableUrlAccessand does not receive those flags.Control path showing intended enforcement:
lib/mail-composer/index.js:358-359,lib/mail-composer/index.js:367-368, and sibling child-node creation paths passdisableUrlAccessanddisableFileAccessintoMimeNode.lib/mime-node/index.js:51-52stores those flags.lib/mime-node/index.js:984-995rejects file paths withEFILEACCESSwhendisableFileAccessis set.lib/mime-node/index.js:998-1009rejects URLs withEURLACCESSwhendisableUrlAccessis set.test/mail-composer/mail-composer-test.js:1028-1044includes a normal MIME-streaming test that expects file access to be blocked whendisableFileAccess: true.Additional same-root-cause variant:
lib/mailer/index.js:406-434implements_convertDataImages()forattachDataUrls.lib/mailer/index.js:407-410callsmail.resolveContent(mail.data, 'html', ...)whenattachDataUrlsis enabled andmail.data.htmlis present.mail.resolveContent()delegates toshared.resolveContent()atlib/mailer/mail-message.js:42-44, an object-formhtml: { path: ... }orhtml: { href: ... }can be resolved before the later MIME streaming enforcement sees the content.attachDataUrlsto be enabled, so the main reportable default/common path isjsonTransport; both should be fixed by enforcing access flags inside the pre-resolution helper or passing policy into it.Default/common exposure evidence:
jsonTransportis a shipped runtime transport selected by publiccreateTransportoptions.test/json-transport/json-transport-test.js:9-83demonstrates thatjsonTransportintentionally resolves file-backedhtmland attachments into JSON output.disableFileAccessanddisableUrlAccessare documented by code and tests as security controls and are copied from transport options into message data for all transports.False-positive screening and negative controls:
disableFileAccess: trueanddisableUrlAccess: truetransport options for bothjsonTransportand normalstreamTransportcontrols.jsonTransportread the temporary local fixture file and embedded the content in JSON despitedisableFileAccess: true.streamTransportwith the same attachment anddisableFileAccess: truerejected withEFILEACCESS.jsonTransportfetched a local HTTP listener despitedisableUrlAccess: true.streamTransportwith the same URL anddisableUrlAccess: truerejected withEURLACCESS.127.0.0.1and did not contact external infrastructure.Affected version evidence and uncertainty:
nodemailer8.0.8 at commit15138a84c543c20aa399218534cdbbfa2ea1ce55.jsonTransporthas existed since commitd78b63b(2017-02-09, "Added test for json transport"), anddisableFileAccessappears in historical setup commit6218b8d(2017-01-31), but older versions were not dynamically tested during this audit.Severity rationale:
pathorhreftriggers the bypass whenjsonTransportis used.Final self-review:
127.0.0.1HTTP listener.streamTransportrejects the same inputs withEFILEACCESSandEURLACCESS.PoC
From a clean checkout of
nodemailerat commit15138a84c543c20aa399218534cdbbfa2ea1ce55, run:Observed output in this environment:
Expected vulnerable output:
JSON_FILE_BYPASS=true, the printed temporary marker inJSON_FILE_CONTENT, aLOCAL_HTTP_REQUEST=GET /privateline, andJSON_URL_BYPASS=true. Expected negative/control output:STREAM_FILE_CONTROL=EFILEACCESSandSTREAM_URL_CONTROL=EURLACCESS, showing the same policy flags work in the normal streaming transport.Cleanup: the PoC removes its temporary fixture file before exiting and closes the local HTTP server.
Impact
If an application uses
jsonTransportto safely serialize or queue partially user-controlled Nodemailer message objects while relying ondisableFileAccess/disableUrlAccess, an attacker can bypass those protections. The file-read variant can copy local file contents into the generated JSON message output. The URL-fetch variant can force outbound HTTP requests from the application host to local or internal services despite URL access being disabled. The impact depends on what message fields the embedding application exposes and where it stores or returns the generated JSON, but the local PoC confirms both protected sink operations are reached.Suggested remediation
Enforce
disableFileAccessanddisableUrlAccessinsideshared.resolveContent()or pass an explicit policy object into every pre-resolution call and reject protectedpath/hrefvalues before opening files or fetching URLs. Apply the same fix tojsonTransportnormalization and theattachDataUrlspre-plugin path. Add regression tests showingjsonTransportreturnsEFILEACCESS/EURLACCESSfor file and URL content when those flags are set, and thatattachDataUrlscannot resolve object-formhtml.path/html.hrefwhen the corresponding access flag is disabled.Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: CRLF injection in Nodemailer List-* header comments allows arbitrary message header injection
GHSA-268h-hp4c-crq3
More information
Details
Summary
Nodemailer constructs
List-*headers from the caller-providedlistmessage option using internally prepared header values. Thelist.*.commentfield is inserted into those prepared values without removing CR (\r) or LF (\n) characters. Because prepared headers bypass the normal header-value sanitizer and are passed tomimeFuncs.foldLines(), a CRLF sequence in a list comment is emitted as an actual header boundary in the generated RFC822 message.An application that lets a lower-privileged or unauthenticated user influence
list.help.comment,list.unsubscribe.comment,list.subscribe.comment,list.post.comment,list.owner.comment,list.archive.comment, orlist.id.commentcan therefore be made to generate messages containing attacker-chosen additional headers.Details
Source-to-sink evidence:
lib/mailer/mail-message.js:241-249calls_getListHeaders(this.data.list)and adds each returned value withthis.message.addHeader(listHeader.key, value).lib/mailer/mail-message.js:253-296builds each list header value as{ prepared: true, foldLines: true, value: ... }.List-ID,lib/mailer/mail-message.js:272-279copiesvalue.commentinto the generated header value. IfmimeFuncs.isPlainText(comment)returns true, it wraps the comment in quotes rather than encoding or CRLF-normalizing it.List-*headers,lib/mailer/mail-message.js:283-288copiesvalue.commentinto(<comment>). IfmimeFuncs.isPlainText(comment)returns true, the value is not encoded or CRLF-normalized.lib/mime-node/index.js:323-351accepts the prepared header object.lib/mime-node/index.js:533-540trustsoptions.prepared; whenfoldLinesis set, it pushesmimeFuncs.foldLines(key + ': ' + value)directly into the header block.lib/mailer/mail-message.js:299-308removes whitespace and angle brackets fromlist.*.url, so the confirmed injection source is thecommentfield, not the URL field.Default/common exposure evidence:
lib/nodemailer.js:21-60exposes the publiccreateTransport(...).sendMail(...)flow used by the package.examples/full.js:106-123documentslist.unsubscribe.commentandlist.id.commentas normal message options.False-positive screening and negative controls:
subjectheader input containing CRLF was normalized to a singleSubject:header and did not createX-Injectedin the local control case.prepared: truecustom headers are an explicit low-level escape hatch, but this issue is different because Nodemailer itself creates prepared headers from the documentedlist.*.commentoption.Variant analysis:
Local testing confirmed the same root cause for comments in
List-Help,List-Unsubscribe,List-Subscribe,List-Post,List-Owner,List-Archive, andList-ID. These should be fixed together by rejecting or normalizing CR/LF in list comments before prepared header generation, or by avoiding the prepared-header bypass for caller-controlled list values.Affected version evidence and uncertainty:
nodemailer8.0.8 at commit15138a84c543c20aa399218534cdbbfa2ea1ce55._getListHeaderspresent in historical commits including22fcff8(v4.3.0) and related list-header work in9b4f90a(v3.1.8), but older versions were not dynamically tested during this audit.Severity rationale:
Final self-review:
streamTransportPoC and a negativeSubjectcontrol case.PoC
From a clean checkout of
nodemailerat commit15138a84c543c20aa399218534cdbbfa2ea1ce55, run:Observed output in this environment:
Expected vulnerable output:
POSITIVE_HAS_INJECTED=trueand all listed variants ending in=true. Expected negative/control output:CONTROL_HAS_INJECTED=false, showing the ordinarySubjectheader path does not create a separate injected header.Cleanup: none required; the PoC uses only in-memory message generation.
Impact
A lower-privileged attacker who can influence
list.*.commentfields in an application using Nodemailer can inject arbitrary additional headers into generated email messages. This can alter message semantics and downstream mail-client or mail-filter behavior, including adding attacker-controlled metadata headers. The PoC confirms header-boundary injection in the generated RFC822 output; it does not demonstrate SMTP command injection, recipient injection, or code execution.Suggested remediation
Normalize or reject CR and LF in
list.*.commentbefore constructing preparedList-*headers. Prefer sharing the same CRLF-neutralization behavior used for ordinary header values, or avoid usingprepared: truefor caller-controlled list comment content. Add regression tests for CRLF in every documentedlistcomment-bearing field and verify that generated messages do not contain attacker-controlled standalone headers.Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: Message-level raw option bypasses disableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message
CVE-2026-82659 / GHSA-p6gq-j5cr-w38f
More information
Details
Message-level
rawoption bypassesdisableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the sent messagenodemailerv9.0.0 (HEAD4e58450eb490e5097a74b2b2cce35a8d9e21856e)Summary
Nodemailer exposes
disableFileAccessanddisableUrlAccessso an application that passesuntrusted message data to the library can forbid that data from reading local files or
fetching URLs. Every attachment, alternative,
html/text/watchHtml/ampandicalEventcontent node honors these flags. The message-level
rawoption does not.MailComposer.compile()builds the root MIME node for arawmessage without threading thetwo flags, so a
raw: { path: '/etc/passwd' }orraw: { href: 'http://169.254.169.254/…' }message is read / fetched anyway, and the file or HTTP-response bytes become the actual
message that is sent by every transport (SMTP, SES, sendmail, stream, JSON). An actor whose
input the application intended to sandbox therefore obtains arbitrary local-file disclosure and
a full-response SSRF primitive, delivered to a recipient the same actor can choose.
This is the same vulnerability class as the already-published jsonTransport advisory
GHSA-wqvq-jvpq-h66f, but a distinct code path (
rawroot node, notnormalize()), andstrictly higher impact: the jsonTransport bug only affected the locally-returned JSON, whereas
this affects the delivered RFC822 message for all transports.
Affected component
lib/mail-composer/index.js:34-35— root cause:MimeNodeis constructed with only{ newline }. Compare the sibling node builders_createMixed/_createAlternative/_createRelated/_createContentNode(
lib/mail-composer/index.js:389-527), which all passdisableUrlAccess: this.mail.disableUrlAccess, disableFileAccess: this.mail.disableFileAccess.lib/mime-node/index.js:51-52— the constructor derivesthis.disableFileAccess/this.disableUrlAccesssolely from its ownoptions; children do not inherit a parent'sflags (
createChild/appendChild, lines 175-194, pass options through verbatim).lib/mime-node/index.js:812—setRaw()content is resolved throughthis._getStream(this._raw).lib/mime-node/index.js:984-1010—_getStreamreads the file (fs.createReadStream, 995) orfetches the URL (
nmfetch, 1009) only guarded bythis.disableFileAccess/this.disableUrlAccess,which on the
rawroot node arefalse.lib/mailer/index.js:188(
mail.message = new MailComposer(mail.data).compile()), so every transport is affected.Reachability gate (hop-by-hop)
transporter.sendMail({ raw: <userControlled> , to: <userControlled> })with
disableFileAccess: trueand/ordisableUrlAccess: trueconfigured on the transporter(forced onto
mail.datainlib/mailer/mail-message.js:36-40) or per message. This is theexact scenario the flags exist for — the same precondition under which GHSA-wqvq-jvpq-h66f was
accepted.
_createContentNodecarriesdisableFileAccess, so_getStreamthrowsEFILEACCESS.Bypass: the
rawbranch (compile():34-35) never sets the flag on its node, sothis.disableFileAccess === falseand the guard atmime-node:985/:999is skipped.There is no other validation between
mail.rawand the read;rawcontent shapes(
{path},{href}, stream, string, buffer) are accepted as-is bysetRaw/_getStream.fs.createReadStream(content.path)(file disclosure) ornmfetch(content.href, …)(SSRF). The resulting bytes are emitted as the message body bycreateReadStream(), which every transport pipes to its destination(
smtp-transport:233,smtp-pool/pool-resource:208,ses-transport:96,sendmail-transport:184,stream-transport:67).No guard blocks the chain; the only guard (the access flags) is structurally absent on this node.
Root cause
Inconsistent enforcement: the access policy is applied per-
MimeNodevia constructor options andmust be re-passed at every node creation. The
raw-message shortcut incompile()omits it,while all five other node builders include it. The flags are therefore enforced for every content
type except the one that lets the caller supply a complete message body by path/URL.
Exploit path
Application that sandboxes untrusted mail input (
disableFileAccess/disableUrlAccessset):raw: { path: '/proc/self/environ' }(or any server file:/app/.env, key material, etc.) andto: [email protected].compile()builds the raw root node without the flags; the transport reads the file and sendsits contents as the message → arbitrary server-file exfiltration to an attacker-chosen mailbox.
raw: { href: 'http://127.0.0.1:8080/admin' }or a cloud metadata URL →Nodemailer fetches it server-side and delivers the full response body in the email →
full-response SSRF (no blind-channel limitation).
Impact
full-response SSRF to internal/metadata endpoints, also disclosed in the message.
ineffective for
raw.Preconditions
The application (a) passes
disableFileAccessand/ordisableUrlAccess(the documented sandboxingflags) and (b) lets untrusted input influence the
rawfield (and, for maximal disclosure,to).No other configuration is required; all bundled transports are affected. This mirrors the accepted
precondition of GHSA-wqvq-jvpq-h66f.
Severity
rawobject; deterministic.flags are set); not fully anonymous in the typical deployment.
recipient. (The sibling jsonTransport advisory used C:L because its leak stayed in locally-returned
JSON; here the bytes leave the system in the sent message, so C:H is warranted.)
Note: if a deployment fixes the recipient (
tonot attacker-controlled) the disclosure channelnarrows and the rating degrades toward the sibling's Medium; the High rating reflects the
reasonable worst case where
rawandtoare both untrusted.Adversarial re-read (attempts to refute)
rawcontent is by-design trusted, so the flags shouldn't apply." Rejected: every othercontent path (attachments, alternatives, html/text, icalEvent) honors the flags, and the
maintainer already accepted GHSA-wqvq-jvpq-h66f for exactly this "untrusted input + flag set"
model. The asymmetry — attachment
{path}is blocked butraw:{path}is not — is the bug, andthe PoC's CONTROL case proves the flag is otherwise effective on the same file.
compile():35constructs the node with
{ newline }only;MimeNodeconstructor setsthis.disableFileAccess = !!options.disableFileAccess→false;rootNodeis itself; noinheritance exists.
attachments:[{path}],same file, same transporter) returns
EFILEACCESS; only theraw:{path}message leaks. Thesentinel nonce exists solely in the temp file; the URL nonce is generated server-side and is only
obtainable by an actual fetch. Both observables are uniquely bound to the bypass.
streamTransportand the root cause is inMailComposer.compile()(mailer:188), shared by alltransports; jsonTransport is a different (already-fixed) path.
I could not find any guard that blocks the chain; the finding survives.
Proof of concept (safe, benign)
findings/nodemailer/raw/poc-raw-fileaccess-bypass.js— local, no network egress (loopback only),no destructive action. Output:
Run:
node findings/nodemailer/raw/poc-raw-fileaccess-bypass.js(exit 0 = confirmed).Remediation
Thread the access policy onto the
rawroot node, exactly as the other builders do:(Defense in depth:
setRaw/_getStreamcould also refuse{path}/{href}raw content when eitherflag is set, regardless of how the node was constructed.) Add a regression test asserting that
raw:{path}andraw:{href}reject withEFILEACCESS/EURLACCESSwhen the flags are set, mirroringthe attachment tests.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain
GHSA-cc9r-2j5m-2m83
More information
Details
Summary
Nodemailer's email-address parser treats an RFC 5322 comment
( ... )inside the domain as a point to concatenate the surrounding text, rather than as folding whitespace (CFWS) that terminates the domain. Consequently a recipient address such as[email protected](x)evil.comis parsed and delivered togood-corp.comevil.com(registrable domaincomevil.com, attacker‑controlled), while a conformant RFC 5322 parser terminates the domain at the comment and readsgood-corp.com.An application that decides whether it is allowed to email a recipient by parsing/validating the recipient's domain — with a strict RFC 5322 parser (used without inspecting parse defects) or with a naive prefix/substring allow‑list — and then hands the raw address to Nodemailer for delivery, can be induced to send mail to a domain the attacker controls. This is an Interpretation Conflict (CWE‑436), the same class as CVE‑2025‑13033, reached through the RFC 5322 comment construct (the "Comments" technique in PortSwigger's Splitting the email atom research, which produced a Postfix fix).
Severity is Moderate: exploitation requires the app's domain check to disagree with Nodemailer (see Impact for exactly which parsers do and do not). Verified end‑to‑end against a real RFC 5321 SMTP server (nodemailer 9.0.6 →
aiosmtpd).Details
Root cause is in
lib/addressparser/index.js.Tokenizer.operators):)is immediately followed by a non‑break character (anything other than space / tab / CR / LF /,/;), the tokenizer marks that operator token withnoBreak = true:_handleAddressthen glues the token that follows the comment onto the token that preceded it (dropping the comment):For the input
[email protected](x)evil.comthe tokens aretext:"[email protected]",op:"(",text:"x",op:")"(flaggednoBreak),text:"evil.com". Step 3 appendsevil.comonto[email protected], producing the single domaingood-corp.comevil.com. The comment content (x) is discarded into the display‑name field.RFC 5322 defines a comment as CFWS — semantically folding whitespace — and it may not appear inside a
dot-atom. A comment therefore separates tokens and terminates the domain; the conformant reading ofgood-corp.com(x)evil.comis the domaingood-corp.com(with the trailingevil.combeing invalid/ignored). Nodemailer instead concatenates the two atoms across the removed comment, yielding a different, attacker‑registrable domain.Nodemailer uses the parsed address for both the SMTP envelope (
getEnvelope()→RCPT TO) and the emittedTo:/From:headers, so the entire message is routed to the concatenated domain.Related grammar defect (bonus, lower impact): nested comments are legal in RFC 5322, but the tokenizer closes the comment at the first
)(chr === this.operatorExpecting, line ~392), so a valid nested comment such as[email protected](a(b)c)is mis‑balanced and mangled tox.comc). That particular output contains a stray)and is rejected by a conformant MTA (501) — a bounce/robustness issue, not a misroute.Suggested fix: treat a comment as folding whitespace that terminates the current token — i.e. do not propagate
noBreakacross a comment‑closing)(restrict thenoBreakoptimization to quoted‑string closes), and support nested comments per RFC 5322. Equivalently, never emit a domain formed by concatenating two atoms that were separated only by a comment.PoC
Environment: Node.js ≥ 18 and the published
[email protected]. No special transport configuration is required; the discrepancy is in address parsing.poc-comment.js:Run:
Actual output (nodemailer 9.0.6):
The application asked to mail
[email protected](x)evil.com; Nodemailer delivers togood-corp.comevil.com— registrable domaincomevil.com, which an attacker can register.Verified against a real RFC 5321 server (containerized lab included with this report —
docker compose up --build, caseR8_comment_glue, receiver =aiosmtpd):Which parser sees what (the crux of exploitability):
[email protected](x)evil.comemail.policy.default(strict RFC 5322)good-corp.com(flagsInvalidHeaderDefect)startsWith/includes('@good-corp.com'))good-corp.comaddressparsergood-corp.comevil.comemail.utils.getaddressesgood-corp.comevil.comurl.domainToASCIIgood-corp.com(x)evil.comImpact
@good-corp.com", employee‑only flows, "same‑tenant" routing). Applications that validate with Nodemailer's ownaddressparser,email.utils.getaddresses, orurl.domainToASCIIare not affected, which is why this is rated below the IDN/Punycode issue.Patched in 9.1.0
Fixed in 902b63e.
Not propagating
noBreakacross the closing)on its own breaks valid addresses, because CFWS is legal on either side of the@:user@(x)good-corp.comanduser(x)@good-corp.comboth come out mangled. A comment now joins what it separates only when one side carries the@, so those keep resolving while[email protected](x)evil.comterminates atgood-corp.com.Quoted-string and angle-address joining are unchanged. Nested comments are still not modelled, but the misroute is gone:
[email protected](a(b)c)now yields[email protected].Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: IDN/Punycode domain allow-list bypass leads to email delivery to an attacker-controlled domain
GHSA-wmmp-3585-3rmp
More information
Details
Summary
Nodemailer resolves an international (IDN / non-ASCII) recipient domain to a different Punycode
xn--label than every UTS‑46‑conformant parser (web browsers, the WHATWG URL Standard, Node'surl.domainToASCII, Python'sidna). Its address normalizer (_normalizeAddressinlib/mime-node/index.js) uses the bundled raw RFC‑3492 Punycode codec with no UTS‑46 mapping/normalization, so a domain that a standards‑compliant validator maps to a trusted domain is delivered by Nodemailer to a different, attacker‑registrable domain.An application that applies a domain allow‑list / same‑domain check to a recipient using a normal IDN‑aware parser (or that shows the normalized recipient to a user for confirmation) and then relies on Nodemailer to deliver to that domain can be induced to send email to an unintended external domain. This is the same weakness class as CVE‑2025‑13033 (Interpretation Conflict, CWE‑436) but reached through IDN/Punycode rather than quoted local‑parts, and it is not addressed by the 7.0.7 fix.
Because the mismatch can be triggered with an invisible character (U+00AD SOFT HYPHEN) that UTS‑46 folds away to the exact trusted domain string, no visible look‑alike/homograph is required.
Details
lib/mime-node/index.js→_normalizeAddress(address)(around lines 1307–1346) splits the address at the last@and normalizes the domain like this:punycodehere is the project’s bundled codec (lib/punycode/), which is a pure RFC 3492 (Punycode) implementation. The only normalization applied to the domain is.toLowerCase(). It performs none of the UTS‑46 “IDNA2008 + compatibility processing” steps that browsers and DNS‑facing resolvers apply before Punycode encoding, specifically:U+00ADSOFT HYPHEN,As a result, for any domain containing a UTS‑46‑mapped or ‑ignored character, Nodemailer’s
punycode.toASCII(...)produces a different A‑label thanurl.domainToASCII(...)(Node ≥ 7 / WHATWG),new URL('http://'+domain), browsers, and Python’sidna(uts46=True). Nodemailer then uses its A‑label as:RCPT TO:<local@xn--…>(getEnvelope()→lib/smtp-connection/index.js_setEnvelope), andTo:/From:headers (_convertAddresses).So the domain a standards‑compliant validator computes and the domain Nodemailer actually delivers to disagree, on a syntactically valid, validator‑accepted address. Concrete divergences (verified on 9.0.6):
url.domainToASCII)victim@compa{U+00AD}ny.com(invisible soft hyphen)company.comxn--company-pka.comvictim@company.com(full‑width)company.comxn--mi7cd4afch9d.comuser@exámple.com(NFDa+U+0301)xn--exmple-qta.comxn--example-vge.comThis is the “Punycode / IDN parser discrepancy” technique documented in PortSwigger’s Splitting the email atom research (which produced e.g. Joomla CVE‑2024‑21725 and fixes in the PHP
idna_convertlibrary). The fix for CVE‑2025‑13033 (nodemailer 7.0.7) hardened the quoted‑local‑part path only; this IDN path is independent and still present in 9.0.6 (latest) and, given the long‑standing use of the bundled RFC‑3492 codec, earlier releases.Suggested remediation: perform UTS‑46 processing before/at domain encoding so Nodemailer’s resolution matches browsers, validators, and DNS — e.g. use the runtime’s
url.domainToASCII()(available since Node 7) instead of the rawpunycode.toASCII, and decode with the matching UTS‑46domainToUnicode. At minimum, reject a domain whose value changes under UTS‑46 mapping (i.e.punycode.toASCII(d)≠url.domainToASCII(d)).PoC
Environment: Node.js ≥ 18, the published
[email protected]. No special configuration; the discrepancy is in domain normalization itself.poc-idn.js:Run: