Repository navigation
fix(deps): update security updates [security] (major) - #3769
Merged
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
felipefreitag
approved these changes
Oct 6, 2026
This branch was successfully deployed
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:
^5.2.2→^6.0.09.0.1→10.0.6basic-ftp allows a malicious FTP server to cause client-side denial of service via unbounded multiline control response buffering
CVE-2026-44240 / GHSA-rpmf-866q-6p89
More information
Details
Summary
basic-ftpis vulnerable to client-side denial of service when parsing FTP control-channel multiline responses.A malicious or compromised FTP server can send an unterminated multiline response during the initial FTP banner phase, before authentication. The client keeps appending attacker-controlled data into
FtpContext._partialResponseand repeatedly reparses the accumulated buffer without enforcing a maximum control response size.As a result, an application using
basic-ftpcan remain stuck inconnect()while memory and CPU usage grow under attacker-controlled input. This can lead to process-level denial of service, container OOM kills, worker restarts, queue backlog, or service degradation in applications that automatically connect to FTP endpoints.Details
Root cause
The root cause is that incomplete FTP multiline control responses are buffered without an upper bound.
FtpContextstores incomplete control-channel data in_partialResponse:https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/FtpContext.ts#L63-L64
Incoming control-channel data is handled in
_onControlSocketData. The implementation concatenates the previous incomplete response with the new chunk, parses the entire accumulated string, and storesparsed.restback into_partialResponse:https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/FtpContext.ts#L328-L340
The relevant flow is:
completeResponse = this._partialResponse + chunk
parsed = parseControlResponse(completeResponse)
this._partialResponse = parsed.rest
There is no maximum size check before concatenating, before parsing, or before storing
parsed.rest.The parser accepts incomplete multiline responses and returns the entire unterminated multiline group as
rest:https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/parseControlResponse.ts#L15-L43
If a server starts a multiline FTP response:
220-malicious banner starts
but never sends the terminating line:
220 ready
then
parseControlResponse()treats the accumulated multiline data as incomplete and returns it asrest.Because
_onControlSocketData()feeds_partialResponse + chunkback into the parser on every new data event, the client repeatedly reparses a growing attacker-controlled buffer. This creates both memory growth and increasing parsing work.Why this is security-relevant
The vulnerable component is a client library. The attacker does not need to authenticate to the victim system and does not need valid FTP credentials.
The attack occurs automatically when an application using
basic-ftpconnects to a malicious or compromised FTP server. The malicious response is sent as the FTP server banner before login. No additional user interaction is required after the application initiates a normal FTP connection.This is realistic for applications that use FTP for:
In those environments, one malicious or compromised FTP endpoint can cause the Node.js process using
basic-ftpto consume excessive memory and CPU or remain stuck in a pending connection state.Proof of Concept
The PoC uses a local malicious FTP server that accepts a victim connection and sends an unterminated multiline FTP banner. The banner starts with
220-, but the server never sends the required terminating220line.Reproduction steps
From the root of the
basic-ftpproject:poc_control_parser_direct.js
CHUNKS=1000 node poc_control_parser_direct.js | tee poc-results/parser_direct_1000.logparser_direct_1000.log
Run the end-to-end malicious FTP server PoC:
poc_control_multiline_dos.js
CHUNK_SIZE=8192 CHUNKS=1000 DELAY_MS=1 node poc_control_multiline_dos.js | tee poc-results/control_multiline_dos_1000.logcontrol_multiline_dos_1000.log
Observed result: parser-only PoC
This shows that
parseControlResponse()retained the full unterminated multiline response asrest.The retained buffer grew to
7.81 MiB. Heap usage increased from3.69 MiBto80.77 MiB, and RSS increased from54.77 MiBto141.64 MiB.Observed result: end-to-end malicious FTP server PoC
Only
7.8 MiBof malicious control-channel data was sent. The client retained7.8 MiBin_partialResponse, showed large memory spikes, and remained pending insideconnect()because the multiline response was never terminated.Expected behavior
The client should enforce a maximum size for incomplete FTP control responses. If the accumulated multiline response exceeds a safe limit, the client should close the connection and reject the active task with an error.
The client should not allow a remote FTP server to make
_partialResponsegrow without bound.Actual behavior
A malicious FTP server can keep the client in a pending connection state by sending an unterminated multiline control response.
basic-ftpcontinues buffering and reparsing the accumulated data without a maximum response size.Impact
A malicious or compromised FTP server can cause denial of service in applications using
basic-ftp.Possible real-world impact includes:
Threat model
The attacker controls, compromises, or can impersonate an FTP server that a victim application connects to.
Examples:
In each case, the victim application initiates a normal FTP connection. The malicious server sends an unterminated multiline banner before authentication. The vulnerable client then buffers and reparses the response indefinitely.
No FTP credentials are required for exploitation because the attack happens before login.
Suggested fix
Introduce a maximum control response buffer size, especially for incomplete multiline responses.
Recommended changes:
maxControlResponseBytesormaxControlResponseLengthlimit.Example defensive logic:
A regression test should verify that a response beginning with
220-and never terminating with220is rejected after the configured size limit instead of being retained indefinitely.Suggested regression test scenario
A test server should:
220-malicious banner\r\n.220line._partialResponsedoes not grow without bound.Credit request
If you publish an advisory or assign a CVE, please credit me as:
Ali Firas (thesmartshadow) - https://www.smartshadow.dev
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
basic-ftp: Quadratic-time CPU denial of service in Client.list() Unix directory-listing parser (RE_LINE backtracking)
CVE-2026-102990 / GHSA-c475-qrg2-pj4r
More information
Details
Summary
Client.list()parses the server's directory listing with the Unix-style parser inparseListUnix.js. ItsRE_LINEregex has two adjacent(\S+(?:\s\S+)*)groups (owner name, then group name) followed by a required numeric size group. When a line starts with a valid listing prefix but the tokens after it never satisfy the size and date fields, the engine backtracks over every way of splitting those tokens between the two groups before it can fail, so matching one line costs roughly O(n²) in the line's length.The server whose directory a client lists controls that listing, so it can return one line that pins the Node.js event loop for as long as it likes.
parseList()picks the parser from the last non-blank line only, then runs it on every line, so a normal line placed last selects the Unix parser and a crafted line earlier hits the quadratic match.Proof of concept
npm i basic-ftp && node repro.js:Prints
list() blocked 39.75s; heartbeats fired: 0, versus ~0.06s for a normal listing. The event loop is frozen the whole time. Cost is quadratic: 32 KB ≈ 2.4s, 64 KB ≈ 9.6s, 128 KB ≈ 39s.maxListingBytesdefaults to 40 MB, so a single line can be far larger, and ~1 MB already blocks for tens of minutes.Impact
One directory listing freezes the whole process, under default options, through the primary API. This is the same "malicious FTP server causes client-side denial of service" shape as GHSA-rp42-5vxx-qpwr, also in
Client.list()and rated high. The byte cap added there bounds memory, not the parser's CPU cost.Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: resolveContent() on a MailMessage bypasses disableFileAccess/disableUrlAccess when called with the legacy signature
GHSA-8m3c-c648-2xjj
More information
Details
Summary
Nodemailer's
disableFileAccess/disableUrlAccessoptions are a security sandbox that lets an application forbid untrusted message content (html/text/attachmentpath/href) from reading local files or making outbound HTTP(S) requests. The fix for GHSA-wqvq-jvpq-h66f (commit5f69497) threaded these flags through the library's internal resolution paths (MailMessage.resolveAll()and_convertDataImages()), but the public plugin APIMailMessage.resolveContent(...args)(lib/mailer/mail-message.js:41-43) remains a raw passthrough toshared.resolveContent().When called with the documented legacy signature
mail.resolveContent(data, key, callback),shared.resolveContentnormalizes the missing options argument to an empty object (options = options || {},lib/shared/index.js:530). The message-level flags that theMailMessageconstructor already copied intomail.data(lib/mailer/mail-message.js:34-38) are silently discarded, soresolveContentValueskips both access-control guards and reachesnmfetch(url)(SSRF,lib/shared/index.js:588) orfs.createReadStream(path)(arbitrary file read,lib/shared/index.js:597).A plugin or application code that resolves message content through the documented API (the same API the library's own
_convertDataImagesuses, threading the flags explicitly) thereby bypasses the sandbox an application deliberately enabled.Details
Root cause. The
MailMessageconstructor stores the transporter-level sandbox flags on the message object (lib/mailer/mail-message.js:34-38):The public resolver is a pure passthrough (
lib/mailer/mail-message.js:41-43):shared.resolveContentsupports the legacy 3-argument signature and collapses the missing options to{}(lib/shared/index.js:524-530):resolveContentValuethen checksoptions.disableUrlAccess/options.disableFileAccess(lib/shared/index.js:581/:590), bothundefinedfor the legacy signature, so it falls through tonmfetch(:588) orfs.createReadStream(:597).Contrast with the fixed paths.
resolveAll()(lib/mailer/mail-message.js:112-115) and_convertDataImages()(lib/mailer/index.js:437-440) both pass the message flags explicitly. The MIME streaming path (lib/mime-node/index.js:1059-1077) also honors the flags. So an application that enables the sandbox and then callstransporter.sendMail()is protected; the bypass appears only when message content is resolved through the public legacy-signature API — which is the documented plugin usage (theresolveContentJSDoc atlib/shared/index.js:510-523states it is "useful when you want to create a plugin that needs a content value").Affected versions. Confirmed on
9.1.0(HEADefd6e29c10c6e0c25c57bd2f2a71302838235a4f, the current npm latest). The gap was introduced by the GHSA-wqvq-jvpq-h66f fix and is still present; the public API has no regression coverage (test/mailer/mail-message-test.jscontains noresolveContenttest).PoC
Requires:
nodemailer@9.1.0, a readable local file, and any reachable HTTP endpoint (loopback suffices). Non-destructive; no network egress beyond a local listener.Observed output on the audit environment (Node 22,
nodemailer@9.1.0):The negative controls (
resolveAll, andresolveContentwith explicit{ disableFileAccess: true }) returnEFILEACCESS, proving the sandbox works on the protected paths and only the legacy-signature passthrough is bypassed. The same bypass reproduces inside a realtransporter.sendMail()flow when acompileplugin callsmail.resolveContent(mail.data, 'html', cb)/mail.resolveContent(mail.data.attachments, 0, cb).Impact
An application that enables
disableFileAccess/disableUrlAccessto contain untrusted message content and that resolves content through the documented plugin API (mail.resolveContent(data, key, callback)) has its sandbox silently bypassed:html/attachmentpathpointing at a server file (/etc/passwd,.env, key material) is read and returned to the caller / delivered in the message.hrefpointing at an internal or loopback URL is fetched from the application host.Reachability precondition: the sandbox flags must be enabled (default off) and the application or its plugin must invoke the documented legacy-signature API on attacker-influenced data. The default
transporter.sendMail()path remains protected, so this is a defense-in-depth gap in the library's own access-control enforcement rather than a default-flow bypass. It is the same vulnerability class as the previously accepted GHSA-wqvq-jvpq-h66f (CVE-2026-82660) and GHSA-p6gq-j5cr-w38f (CVE-2026-82659), on a distinct third code path.Severity
CVSS:3.1/AV:N/AC:H/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 asuser@good-corp.com(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
user@good-corp.com(x)evil.comthe tokens aretext:"user@good-corp.com",op:"(",text:"x",op:")"(flaggednoBreak),text:"evil.com". Step 3 appendsevil.comontouser@good-corp.com, 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 asuser@x.com(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
nodemailer@9.0.6. 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
user@good-corp.com(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):
user@good-corp.com(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 whileuser@good-corp.com(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:
user@x.com(a(b)c)now yieldsuser@x.com.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: Quadratic (O(n²)) time complexity in addressparser allows remote denial of service via a crafted address list
GHSA-2x7j-588g-ccc2
More information
Details
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. aTo,Cc,Bcc,From, orReply‑Tovalue, or any value passed to the exportedaddressparser) 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
addressparsertokenizes 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):Array.prototype.concatbuilds and returns a new array containing a copy of every element accumulated so far. ReassigningparsedAddresses = parsedAddresses.concat(handled)on each of the n iterations copies 1 + 2 + 3 + … + n elements in total, i.e. O(n²) work (and O(n²) transient allocations) for an input containing n addresses. Tokenization and_handleAddressthemselves are linear; the quadratic blowup is entirely this accumulator.Root‑cause proof. Replacing only that line with an in‑place append and re‑running the exact same input:
Measured scaling (nodemailer 9.0.6,
'a@b.com,'.repeat(n)):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, andgetEnvelope()), so a singletransport.sendMail({ to: <crafted string> })triggers it. It is also reached directly through the exportedrequire('nodemailer/lib/addressparser'), which many applications call to validate or display user‑supplied recipient lists. Confirmed via the public API:setHeader('To', 'a@b.com,'.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);(orfor (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
nodemailer@9.0.6. No transport, network, or configuration required — the cost is in parsing.poc-dos.js:Run:
Actual output (nodemailer 9.0.6):
Equivalent trigger through the normal send API (freezes the event loop):
Impact
nodemailer/lib/addressparser) on an address value that can be influenced by an untrusted party — a recipient field in a "send email / invite / share" feature, aReply‑To/Fromderived from user input, a contact‑import or mailing‑list parser, or any endpoint that validates addresses withaddressparser. No authentication, special option, or particular receiver is needed.Patched in 9.1.0
Three separate quadratic paths were fixed, not one:
addressparserrebuilt its accumulator withconcat()on every address (9116da9).'a, b <c@d.com>,'.repeat(n)(same commit).MimeNode#_convertAddresseschecked recipient uniqueness with a linear scan per address (7cc38af, refined in 34da642). This was the most severe of the three and the reported proof of concept did not reach it:'a@b.com,'.repeat(n)is one address repeated, which dedupes to a single envelope entry. A list of distinct recipients cost O(n^2) here, taking ~35s for 100k even afteraddressparserwas fixed.Fixed alongside:
[].concat.applyin_parseAddressesthrewRangeError: Maximum call stack size exceededpast roughly 124k recipients, with no crafted input needed (83b8c48).Parsing 200k addresses now takes ~80ms instead of ~25s, and every path scales linearly. A new
maxRecipientsoption (default 100000) throws rather than truncating, as a backstop.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
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
nodemailer@9.0.6. No special configuration; the discrepancy is in domain normalization itself.poc-idn.js:Run:
Actual output (Nodemailer 9.0.6):
The application’s domain check approves
company.com, but the message is sent toxn--company-pka.com— a different domain an attacker can register — carrying theTo:header<victim@xn--company-pka.com>as well.A containerized version that proves the same result against a real RFC 5321 SMTP server (
aiosmtpd) is included alongside this report (docker compose up --build, casesR6/IDN); the receiving server acceptsRCPT TO:<victim@xn--company-pka.com>and reports the recipient domain asxn--company-pka.com.Impact
Any application that uses Nodemailer to send mail to a recipient whose domain is subjected to a security or trust decision made with a different (UTS‑46‑conformant) parser, and then trusts Nodemailer to deliver to that domain. This includes:
new URL(),url.domainToASCII, a browser‑side check, or an IDN library;company.comdiffers from the deliveredxn--company-pka.com);Patched in 9.1.0
Domain encoding now applies UTS-46 (259c32d), so
victim@company.comresolves tocompany.com, matchingurl.domainToASCIIand browsers.One caveat on the suggested remediation, hardened in b212ac4:
url.domainToASCIIis a WHATWG host parser, not a pure UTS-46 mapper. It terminates the host at/,\\,?and#and percent-decodes. Used unguarded it introduces a worse version of the same weakness, sinceuser@attacker.example/mail.corp.exampleencodes to the deliverableuser@attacker.examplewhere the bundled Punycode codec left it intact and unroutable. Those characters are now kept away from the mapper.On severity, "attacker-registrable" is doing significant work in the report:
xn--company-pka.comdecodes to a label containing U+00AD andxn--mi7cd4afch9d.comto full-width Latin, neither of which Verisign's IDN tables permit for a .com registration. The misdelivery and the confirmation-UI mismatch stand regardless, which is why this is rated level with the comment issue rather than above it.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: Process-global DNS cache reuses TLS
servernameacross transports, enabling cross-tenant SMTP credential disclosureGHSA-6vj9-mwq6-2f5v
More information
Details
Summary
Nodemailer's process-global DNS cache is keyed only by
host, but each cache entry also stores the caller-specific TLSservername. When two direct SMTPS transports use the same DNS host with differenttls.servernamevalues, the first transport's server name is returned to the second transport and overwrites its explicitly configured value.As a result, Nodemailer sends the wrong SNI value and verifies the peer certificate against the wrong identity. In a multi-tenant service or SNI-routed SMTP gateway, one tenant can prime the cache so that a victim transport connects to the attacker's TLS virtual host, accepts the attacker's certificate with
rejectUnauthorized: true, and sends the victim's SMTP credentials to it.Affected component
nodemailer10.0.140d52215aac65b811d7e131bc916f68605efd9d25.0.0and10.0.1>= 5.0.0, <= 10.0.1secure: true) where different transports use the same non-IPhostand different TLSservernamevaluesThe vulnerable cache implementation was introduced in commit
6859b5dd96c8d9f0070a3169a877181b71df4a3bon 2018-12-28. Git history showsv5.0.0as the first release tag containing that commit. The behavior remains present inv10.0.1.Details
Root cause
src/shared/index.tsdefines one module-global DNS cache, keyed only by the DNS host:Although the cache key contains only
host, the cached value contains both DNS addresses and the request-specific TLS identity:On a cache hit,
resolveHostname()returns the cachedservernamewithout considering the current call'soptions.servername:formatDNSValue()copies that stale value into the result:For a direct TLS connection,
SMTPConnection.connect()initially copies the current transport's TLS configuration intoopts._resolveAndConnect()then overwrites every truthy field with the cached resolver result, includingopts.servername:The resulting
optsobject is passed totls.connect(). Node therefore sends the cached server name as SNI and verifies the certificate against that cached name, rather than against the server name explicitly configured for the current transport.The default DNS cache TTL is five minutes:
Code path
Relevant source locations in the tested revision
src/shared/index.ts:184— five-minute default cache TTLsrc/shared/index.ts:245— process-global cache keyed by hostsrc/shared/index.ts:247-262— cachedservernamereturned byformatDNSValue()src/shared/index.ts:292-323— host-only lookup and cache-hit returnsrc/shared/index.ts:350-359— caller-specificservernamestored in host-only cSummary by cubic
Upgrades two dependencies to patched major versions that resolve multiple security advisories:
nodemailer9.0.1 → 10.0.6 in the example app and theget-uri>basic-ftpoverride from^5.2.2→^6.0.0.Dependencies
basic-ftp@6.2.1fixes client-side denial of service from unbounded FTP multiline-response buffering (CVE-2026-44240) and quadratic CPU use inClient.list()(CVE-2026-102990).nodemailer@10.0.6fixes sandbox bypasses fordisableFileAccess/disableUrlAccess, recipient-domain mis-parsing via RFC 5322 comments and IDN/Punycode, quadratic-time address parsing, and a DNS cache that could leak SMTP credentials across transports.pnpm-lock.yamlalso picks up unrelated transitive changes (@types/node25 → 22,acorn8.11 → 8.16 for mintlify snapshots).Written for commit 9747d37. Summary will update on new commits.