fix(deps): update dependency nodemailer to v10 [security] - #4499
renovate-bot wants to merge 1 commit into
Conversation
|
/gcbrun |
There was a problem hiding this comment.
Code Review
This pull request updates the nodemailer dependency to version ^10.0.0 in compute/package.json. The review feedback points out that nodemailer v10.0.0 requires Node.js 20 or newer, which conflicts with the current engines.node requirement of >=16.0.0. It is recommended to update the engines.node field to >=20.0.0 to ensure compatibility.
| "@google-cloud/compute": "^4.0.0", | ||
| "@sendgrid/mail": "^8.0.0", | ||
| "nodemailer": "^9.0.0", | ||
| "nodemailer": "^10.0.0", |
There was a problem hiding this comment.
Nodemailer v10.0.0 and above requires Node.js 20 or newer. However, the engines.node field in this package.json specifies >=16.0.0. Upgrading to nodemailer v10 without updating the engine requirements or ensuring the runtime is at least Node.js 20 will cause compatibility issues on older Node.js versions. Please update the engines.node field to >=20.0.0 to match the new dependency requirements.
References
- When a dependency upgrade requires a higher Node.js version than currently specified in 'engines.node', consider upgrading the 'engines.node' requirement to a supported version (e.g., '>= 18' or '>= 20') instead of reverting the dependency version.
b8ec461 to
962d291
Compare
|
/gcbrun |
This PR contains the following updates:
^9.0.0→^10.0.6Nodemailer: 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 cachesrc/smtp-connection/index.ts:713-729— direct TLS options and resolver callsrc/smtp-connection/index.ts:741-763— cached fields overwrite current connection optionsPoC
Prerequisites
20.20.2)10.0.1No external SMTP server or network access is required.
1. Generate a certificate for only
attacker.testCreate
openssl.cnf:Generate the certificate and private key:
2. Save the following as
poc-dns-cache-servername-confusion.mjsAdjust the two import paths if the PoC is not saved beside the repository checkout.
3. Build and run
From the Nodemailer checkout:
Observed result
{ "attackerConfiguredServername": "attacker.test", "victimConfiguredServername": "victim.test", "serverObservedSniForBothConnections": [ "attacker.test", "attacker.test" ], "serverReceivedCredentials": [ "\\u0000attacker@example.test\\u0000attacker-secret", "\\u0000victim@example.test\\u0000victim-secret" ] }The victim configured
victim.test, but the server observesattacker.testfor both handshakes. The local certificate contains onlyattacker.test, yet the victim connection succeeds withrejectUnauthorized: trueand then sends the victim's username and password.Expected result
The second connection must use
victim.testfor SNI and certificate hostname verification. With the PoC certificate, it should fail with a hostname mismatch before SMTP authentication occurs. It must never transmit victim credentials after validating the peer asattacker.test.Impact
The vulnerability affects long-running applications that create multiple Nodemailer transports in one process and let separate tenants or security domains configure transports that share a DNS
host. A practical example is an email platform whose SMTP gateway uses SNI to route several customer-specific SMTP endpoints behind one hostname.An attacker who can create or exercise one transport can prime the global cache with the shared host and the attacker's
tls.servername. During the cache lifetime, a victim's direct SMTPS connection to that host can:Possession of SMTP credentials may also let the attacker read or change mail account state where the provider reuses those credentials, or send mail as the victim. The exact secondary impact depends on the SMTP provider.
Where the attacker cannot control an SNI virtual host, stale cross-transport SNI can still cause certificate mismatch failures and cross-tenant availability impact.
Preconditions and limitations
hostcache key and differenttls.servernamevalues.secure: true). The STARTTLS upgrade path constructs TLS options separately and is not claimed vulnerable by this report.Suggested remediation
The DNS cache should store DNS data only.
servernameis connection-specific TLS policy and should not be persisted in a cache keyed solely by hostname.One approach is to remove
servernamefromDnsCacheValueand derive the returned value from the current request on every path:As defense in depth,
_resolveAndConnect()should not overwrite an explicitly configuredopts.servernamewith resolver metadata. Keying the cache by both host and server name would avoid this particular collision, but keeping TLS identity out of a DNS-address cache provides a cleaner separation.A regression test should create two direct-TLS transports in the same process with the same DNS host and different explicit server names, then assert that each TLS connection observes and verifies its own configured name regardless of cache order.
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: Nested structured recipient arrays bypass the parser depth limit and cause stack exhaustion DoS
GHSA-8vvx-rff5-p5rq
More information
Details
Submission metadata
nodemailer40d52215aac65b811d7e131bc916f68605efd9d210.0.12.7.2,3.0.0,7.0.11,9.1.1,10.0.1>= 2.7.2, <= 10.0.1Summary
Nodemailer 10.0.1 does not safely process deeply nested arrays supplied through recipient fields such as
to,cc, andbcc.The public
MimeNodeAddressInputtype recursively permits arrays, butMimeNode._parseAddresses()flattens only the outermost array. A remaining nested array is passed toaddressparser(), whoseTokenizercoerces the input with.toString(). NativeArray.prototype.toString()recursively processes every nested array throughjoin()andtoString()until the V8 call stack is exhausted.A valid 10,021-byte JSON recipient value containing one address wrapped in 5,000 arrays causes:
The exception occurs through the normal
sendMail()API before themaxRecipientslimit is evaluated. If the integrating application does not catch the synchronous exception around the completesendMail()invocation, the Node.js worker or server process terminates.No SMTP server, remote host, large attachment, or successful email delivery is required.
This is distinct from GHSA-rcmh-qjqh-p98v / CVE-2025-14874. The previous vulnerability concerned recursive parsing of RFC 5322 group strings. This report uses Nodemailer's structured recipient-array input and never reaches the parser's
MAX_NESTED_GROUP_DEPTHprotection.Security impact
An attacker who can control a recipient field passed to Nodemailer can trigger stack exhaustion using a roughly 10 KB JSON value. Potentially affected integrations include:
sendMail().When the exception is not caught, a single request or queue item can terminate the process handling mail. A process manager may restart the worker, but repeated malicious inputs can keep workers in a restart loop and make the service unavailable.
Applications that explicitly validate recipient values as flat strings or flat arrays before calling Nodemailer are not exposed through this path. Applications that wrap the complete synchronous
sendMail()invocation in exception handling can prevent process termination, although an attacker can still repeatedly force failed jobs.Attack prerequisites
The vulnerability is reachable when:
to,cc, orbcc.The proof of concept uses
jsonTransportonly to avoid requiring an SMTP server. The vulnerable address normalization and envelope construction occur before transport-specific delivery, so the root cause is not limited tojsonTransport.Technical details
1. The public input type accepts recursively nested arrays
At
src/mime-node/index.ts:67, recipient input is recursively defined:Pinned source:
https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L67
This permits values equivalent to:
2.
_parseAddresses()removes only the outermost arrayAt
src/mime-node/index.ts:1359-1385,_parseAddresses()wraps the input with[].concat(addresses)and iterates the resulting outer array:Pinned source:
https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L1359-L1386
For an input nested 5,000 levels deep, the callback receives an array nested 4,999 levels deep. Because this value does not have a truthy
.addressproperty, it is passed directly toaddressparser().3.
Tokenizerinvokes recursive native array conversionThe
Tokenizerconstructor performs the following coercion atsrc/addressparser/index.ts:393:Pinned source:
https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/addressparser/index.ts#L393
When
stris an array, this invokesArray.prototype.toString(). Array string conversion invokesjoin(), which converts every nested element to a string. Deeply nested arrays therefore produce native recursion resembling:At sufficient depth, V8 raises
RangeError: Maximum call stack size exceeded.4. The recipient limit is applied too late
Nodemailer's
maxRecipientsprotection is evaluated only after the message envelope has been constructed:Pinned source:
https://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mailer/index.ts#L414-L417
The exception occurs inside
getEnvelope(), somaxRecipientscannot prevent this condition.Vulnerable code path
Proof of concept
Test environment
Installation
Create
poc.mjs:Trigger
node poc.mjs 5000Observed result
The tested process exits with status code
1.Control case
Running the same code with a nesting depth of 500 succeeds:
node poc.mjs 500Observed result:
The difference between the trigger and control cases is only the nesting depth.
Reproduction notes
JSON.parse()to model data received by an HTTP API or queue worker.jsonTransportis used.Version verification
The proof of concept was executed against several released versions. Each listed version exited with
RangeError: Maximum call stack size exceededat a depth of 5,000:2.7.23.0.07.0.119.1.110.0.1Version
7.0.11is significant because it contains the fix for the previous string-group recursion advisory. Its failure confirms that this report describes a separate surviving path.The proposed affected range is
>= 2.7.2, <= 10.0.1, representing the versions directly confirmed during testing and the continuous vulnerable implementation observed in source history. Earlier releases were not assessed and should not be considered confirmed safe.Difference from GHSA-rcmh-qjqh-p98v / CVE-2025-14874
The previous advisory used a crafted address string containing nested RFC 5322 groups:
Its recursive path was:
That issue was mitigated by adding and propagating a parser recursion-depth counter capped by
MAX_NESTED_GROUP_DEPTH.This report instead supplies a structured JSON array through the public recipient input:
The array conversion happens before any RFC 5322 group parsing. Consequently:
_handleAddress()is not the source of recursion._depthparser option is not incremented.MAX_NESTED_GROUP_DEPTHis never consulted.Previous advisory:
GHSA-rcmh-qjqh-p98v
Suggested remediation
Flatten
MimeNodeAddressInputvalues iteratively before passing scalar values toaddressparser(). Nested arrays should never be implicitly converted to strings.For example, the implementation can maintain an explicit work stack:
Additional hardening options include:
addressparser().sendMail()callback or rejected Promise.maxRecipients.An iterative implementation is preferable because the public TypeScript type is recursive and indicates that nested array structures are accepted inputs. Cycle detection remains necessary for direct JavaScript callers because cyclic arrays cannot originate from JSON but can be constructed in memory.
Suggested regression tests
The fix should cover:
to,cc,bcc,replyTo, and explicit envelope fields.{ name, address }objects.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 the GitHub Advisory Database (CC-BY 4.0).
Nodemailer: Quadratic backtracking in the addressparser free-text fallback allows remote denial of service
GHSA-v53p-9fqp-m79j
More information
Details
Summary
When
addressparserfinds no address by its strict reading, it falls back to pulling one out of the free text with/\s*\b[^@\s]+@[^\s]+\b\s*/. That pattern backtracks quadratically:[^@\s]+is retried from every offset and rescans the run to the next@each time. A single header value holding a long whitespace-free run with no usable@blocks the Node.js event loop for tens of seconds.It is cheaper to exploit than GHSA-prgh-xp8r-p3m5: 273KB is enough for ~43s, where the comment-joined shape needed ~1.5MB for ~10s.
Details
The fallback in
src/addressparser/index.tsran the pattern as a search.[^@\s]+crosses neither whitespace nor@, so from each start offset it scans forward to the next@or to the end of the run and then fails, and the engine simply advances one character and repeats. Three shapes make every offset fail:@at all@has nothing after it@has nothing before itA fourth reaches it with a valid address present but placed past a long run, so the long run is walked before the match is found.
PoC
Measured on 10.0.5:
'[x]'.repeat(40000)(117KB)'[x]'.repeat(40000) + '@'(117KB)'@' + '[x]'.repeat(40000)(117KB)' >' + '>[x][x]'.repeat(40000)(273KB)Impact
Algorithmic-complexity denial of service. Node is single threaded, so the block stalls the whole process. Reachable without authentication anywhere inbound header values are handed to this parser, mailparser being the notable case, and reachable from application input wherever a user-supplied string is used as a message address, since
mime-nodeparsesto,fromandccwhen composing.Patch
The search is replaced by a single linear pass that finds the one offset the pattern can match at, which is then applied there with a sticky regex. Match results are unchanged, verified against the previous implementation over 3.4 million random strings comparing both the match offset and the matched text, plus 800k full-parse comparisons.
Found while validating the report in GHSA-prgh-xp8r-p3m5, not reported externally.
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).
Release Notes
nodemailer/nodemailer (nodemailer)
v10.0.6Compare Source
Bug Fixes
v10.0.5Compare Source
Bug Fixes
v10.0.4Compare Source
Bug Fixes
v10.0.3Compare Source
Bug Fixes
v10.0.2Compare Source
Bug Fixes
v10.0.1Compare Source
Bug Fixes
v10.0.0Compare Source
⚠ BREAKING CHANGES
Features
Bug Fixes
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
v9.0.1Compare Source
Bug Fixes
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.