{"id":"GHSA-prgh-xp8r-p3m5","title":"Nodemailer addressparser: O(n^2) on comment-joined addresses enables a remote DoS (reachable via mailparser)","summary":"Nodemailer addressparser: O(n^2) on comment-joined addresses enables a remote DoS (reachable via mailparser)","severity":"high","cvss":7.5,"cwe":["CWE-400","CWE-407"],"vendor":"nodemailer","product":"nodemailer","ecosystem":"npm","affected":["nodemailer >= 9.1.0, <= 10.0.4"],"patched":["nodemailer 10.0.5"],"published":"2026-09-30","updated":"2026-09-30","sourceUpdated":"2026-09-30T14:41:04Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5","references":[{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-prgh-xp8r-p3m5"},{"url":"https://github.com/nodemailer/nodemailer/commit/c07f17518d25aca8ab2ad66968dcbca538c24b89"},{"url":"https://github.com/nodemailer/nodemailer/releases/tag/v10.0.5"},{"url":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5"}],"tags":["ghsa","npm"],"ingestedAt":"2026-09-30T15:07:05.373Z","slug":"GHSA-prgh-xp8r-p3m5","body":"## Overview\n\n### Summary\n\n`nodemailer/lib/addressparser` parses one shape of address in O(n^2) time. A single ~640 KB address value blocks the Node.js event loop for roughly 7 seconds. And it is reachable without auth: mailparser feeds inbound email headers straight into this parser, so one crafted email is enough to stall a service that parses mail.\n\n### Details\n\nThe parser builds a single address by accumulating its atoms into one string. When the atoms are separated by RFC 5322 comments, like `a@b(c)@b(c)@b(c)...`, every atom re-joins that same growing string.\n\nThe join check in `src/addressparser/index.ts`:\n\n```js\nconst joins =\n    prevToken &&\n    prevToken.noBreak &&\n    parts.length &&\n    (prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');\n```\n\nThe issue is the order of the last two operands. `parts[parts.length - 1].slice(-1)` runs before the cheap `token.value.charAt(0)`. `slice(-1)` has to flatten the accumulator to read its last character → O(current length) → and that runs on every token → O(n^2) over the whole value. Since `||` is left to right, the cheap `charAt(0)` that would short-circuit never gets the chance.\n\nThe chain: long comment-joined address → one growing accumulator → `slice(-1)` re-flattens it on every token → quadratic parse time.\n\n### PoC\n\nIsolated, just the parser (`npm i nodemailer@10.0.3`):\n\n```js\nconst addressparser = require('nodemailer/lib/addressparser');\nconst s = Date.now();\naddressparser('a' + '@b(c)'.repeat(130000));\nconsole.log(Date.now() - s, 'ms'); // ~7000 ms, blocking\n```\n\nEnd to end through mailparser, the remote path (`npm i mailparser@3.9.24`):\n\n```js\nconst { simpleParser } = require('mailparser');\n(async () => {\n  const to = 'a' + '@b(c)'.repeat(130000);\n  const eml = `From: a@b.com\\r\\nTo: ${to}\\r\\nSubject: x\\r\\n\\r\\nhi\\r\\n`;\n  const s = Date.now();\n  await simpleParser(eml);\n  console.log(Date.now() - s, 'ms'); // ~7000 ms, blocking\n})();\n```\n\nTimings measured on 10.0.3:\n\n| Address value | Parse time |\n| --- | --- |\n| 390 KB | 1.3 s |\n| 585 KB | 5.6 s |\n| 640 KB | 6.7 s |\n| 976 KB | 18 s |\n\nIt survives RFC 5322 folding: fold the header at offsets that are a multiple of the atom length and every `)`+`@` junction stays intact, so the payload is a standards-compliant email with lines under 998 octets and still triggers it.\n\n### Impact\n\nAlgorithmic-complexity DoS. Node is single threaded, so the block stalls everything else in the process, and a handful of these back to back keeps a service down.\n\nAffected: anything that runs `addressparser` on attacker-controlled input, either the public export directly or address headers built from user input. The unauthenticated remote case is mailparser. 3.9.24 pins nodemailer 10.0.3 and calls the parser on inbound `To`/`From`/`Cc` with no length cap, so any service parsing inbound mail with it can be frozen by a single email.\n\n### Suggested fix\n\nSwap the last two operands so the cheap check runs first:\n\n```js\n(prevToken.value !== ')' || token.value.charAt(0) === '@' || parts[parts.length - 1].slice(-1) === '@')\n```\n\nPure boolean commutation, so the parse output is identical. Verified byte for byte on the test inputs, and the full 1276-test suite passes.\n\n## Affected packages\n\n- `nodemailer >= 9.1.0, <= 10.0.4`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `nodemailer 10.0.5`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}