Update npm package nodemailer to v9.1.1 [SECURITY] - #9614
Update npm package nodemailer to v9.1.1 [SECURITY]#9614hash-dependencies[bot] wants to merge 1 commit into
nodemailer to v9.1.1 [SECURITY]#9614Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
3 Skipped Deployments
|
PR SummaryLow Risk Overview The upgraded release addresses several advisories on Nodemailer’s mail path (address parsing DoS, IDN/comment-based recipient misrouting, and Reviewed by Cursor Bugbot for commit 2506d75. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2506d75. Configure here.
| "mime-types": "2.1.35", | ||
| "nanoid": "3.3.18", | ||
| "nodemailer": "9.0.1", | ||
| "nodemailer": "9.1.1", |
There was a problem hiding this comment.
Lockfile still pins vulnerable nodemailer
High Severity
package.json now declares nodemailer 9.1.1, but yarn.lock still resolves 9.0.1. CI and Docker both run yarn install --immutable, so the install fails, and the security patches this PR is meant to apply are never installed.
Reviewed by Cursor Bugbot for commit 2506d75. Configure here.


This PR contains the following updates:
9.0.1→9.1.1Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
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).
Release Notes
nodemailer/nodemailer (nodemailer)
v9.1.1Compare Source
Bug Fixes
v9.1.0Compare Source
Features
Bug Fixes
v9.0.6Compare Source
Bug Fixes
v9.0.5Compare Source
Bug Fixes
v9.0.4Compare Source
Bug Fixes
v9.0.3Compare Source
Bug Fixes
v9.0.2Compare Source
Bug Fixes
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate CLI.