{"id":"CVE-2026-92596","aliases":["CLSA-2026-1784596338","CLSA-2026-1789040951","CLSA-2026-1790202437","CLSA-2026-1791511288","CLSA-2026-1791795466","CLSA-2026-1791806342","CLSA-2026-1791806459","CLSA-2026-1791814880","CLSA-2026-1791814986","CLSA-2026-1792115498","DEBIAN-CVE-2026-92596","EUVD-2026-81296","GHSA-268h-hp4c-crq3","GHSA-2x7j-588g-ccc2","GHSA-8m3c-c648-2xjj","GHSA-8vvx-rff5-p5rq","GHSA-cc9r-2j5m-2m83","GHSA-v53p-9fqp-m79j","GHSA-v554-hhrr-96v5","GHSA-wmmp-3585-3rmp","ROOT-APP-NPM-GHSA-268h-hp4c-crq3","ROOT-APP-NPM-GHSA-2x7j-588g-ccc2","UBUNTU-CVE-2026-92596"],"status":"published","title":"Nodemailer before 9.1.0 Denial of Service via addressparser","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","severity":{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00824,"percentile":0.57863,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-20","CWE-400","CWE-407","CWE-436","CWE-93"],"published":"2026-06-15T17:36:06Z","modified":"2026-10-28T20:18:17Z","fixes":[{"ecosystem":"npm","package":"nodemailer","fixed_in":["8.0.9","9.1.0"],"sources":["ghsa","osv"]},{"ecosystem":"TuxCare:npm","package":"nodemailer","fixed_in":["2.7.2-tuxcare.3","6.10.1-tuxcare.8","9.0.5-tuxcare.1","9.0.3-tuxcare.1","9.0.5-tuxcare.2","9.0.2-tuxcare.1"],"sources":["osv"]},{"ecosystem":"Debian:14","package":"node-nodemailer","fixed_in":["10.0.0+~8.0.1-1"],"sources":["osv"]},{"ecosystem":"Root:npm","package":"nodemailer","fixed_in":["6.10.1-aikido.10","6.10.1-aikido.11","7.0.11-aikido.6","7.0.11-aikido.7","6.10.1-aikido.12","7.0.11-aikido.5","6.10.1-aikido.9","6.10.1-aikido.8","6.10.1-aikido.5","7.0.11-aikido.3","7.0.9-aikido.6","6.10.1-aikido.6","7.0.11-aikido.4","7.0.9-aikido.7","7.0.9-aikido.8","6.10.1-aikido.13","7.0.11-aikido.8","7.0.11-aikido.9","6.10.1-aikido.14","9.0.5-aikido.2","7.0.13-aikido.3","9.0.1-aikido.2","9.0.3-aikido.3","7.0.13-aikido.4","9.0.1-aikido.3","9.0.5-aikido.3","9.0.3-aikido.2","8.0.10-aikido.1","8.0.5-aikido.3","8.0.5-aikido.2","9.0.4-aikido.1","8.0.10-aikido.2","9.0.4-aikido.2","8.0.10-aikido.3","8.0.5-aikido.4","9.0.5-aikido.4","7.0.13-aikido.5","9.0.1-aikido.4","9.0.3-aikido.4","7.0.13-aikido.6","7.0.13-aikido.7"],"sources":["osv"]},{"ecosystem":"Root:npm","package":"@rootio/nodemailer","fixed_in":["6.10.1-root.io.10","6.10.1-root.io.11","7.0.11-root.io.6","7.0.11-root.io.7","6.10.1-root.io.12","7.0.11-root.io.5","6.10.1-root.io.9","6.10.1-root.io.8","6.10.1-root.io.5","7.0.11-root.io.3","7.0.9-root.io.6","6.10.1-root.io.6","7.0.11-root.io.4","7.0.9-root.io.7","7.0.9-root.io.8","6.10.1-root.io.13","7.0.11-root.io.8","7.0.11-root.io.9","6.10.1-root.io.14","9.0.5-root.io.2","8.0.10-root.io.1","7.0.13-root.io.3","9.0.1-root.io.2","9.0.3-root.io.3","7.0.13-root.io.4","9.0.1-root.io.3","9.0.5-root.io.3","9.0.3-root.io.2","8.0.5-root.io.3","8.0.5-root.io.2","9.0.4-root.io.1","8.0.10-root.io.2","9.0.4-root.io.2","8.0.10-root.io.3","8.0.5-root.io.4","9.0.5-root.io.4","7.0.13-root.io.5","9.0.1-root.io.4","9.0.3-root.io.4","7.0.13-root.io.6","7.0.13-root.io.7"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"8.0.8"},{"fixed":"8.0.9"}]}]},{"source":"ghsa","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]},{"source":"ghsa","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"6.9.16"},{"fixed":"9.1.0"}]}]},{"source":"ghsa","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.7.2-tuxcare.0"},{"fixed":"2.7.2-tuxcare.3"}]}],"versions":["2.7.2","2.7.2-tuxcare.1","2.7.2-tuxcare.2"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"6.10.1-tuxcare.0"},{"fixed":"6.10.1-tuxcare.8"}]}],"versions":["6.10.1","6.10.1-tuxcare.1","6.10.1-tuxcare.2","6.10.1-tuxcare.3","6.10.1-tuxcare.4","6.10.1-tuxcare.5","6.10.1-tuxcare.6","6.10.1-tuxcare.7"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.7.2-tuxcare.0"},{"fixed":"2.7.2-tuxcare.6"}]}],"versions":["2.7.2","2.7.2-tuxcare.1","2.7.2-tuxcare.2","2.7.2-tuxcare.3","2.7.2-tuxcare.4","2.7.2-tuxcare.5"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.1-tuxcare.0"},{"fixed":"9.0.1-tuxcare.1"}]}],"versions":["9.0.1"],"bundled":4},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.5-tuxcare.0"},{"fixed":"9.0.5-tuxcare.1"}]}],"versions":["9.0.5"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.3-tuxcare.0"},{"fixed":"9.0.3-tuxcare.1"}]}],"versions":["9.0.3"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.6-tuxcare.0"},{"fixed":"9.0.6-tuxcare.1"}]}],"versions":["9.0.6"],"bundled":5},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.5-tuxcare.0"},{"fixed":"9.0.5-tuxcare.2"}]}],"versions":["9.0.5","9.0.5-tuxcare.1"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.2-tuxcare.0"},{"fixed":"9.0.2-tuxcare.1"}]}],"versions":["9.0.2"]},{"source":"osv","ecosystem":"TuxCare:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.0.0-tuxcare.0"},{"fixed":"9.0.0-tuxcare.3"}]}],"versions":["9.0.0","9.0.0-tuxcare.1","9.0.0-tuxcare.2"],"bundled":3},{"source":"osv","ecosystem":"Debian:12","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.0.0+~8.0.1-1","10.0.10+~8.0.1-1","6.10.0+~6.4.17-1","6.8.0+~6.4.6-1","6.9.13+~6.4.14-1","6.9.15+~6.4.16-1","6.9.16+~6.4.16-1","6.9.4+~6.4.9-1","6.9.4+~6.4.9-2","6.9.4+~6.4.9-3","6.9.4+~6.4.9-4","7.0.10+~7.0.2-1","7.0.12+~7.0.5-1","7.0.5+~7.0.1-1","7.0.6+~7.0.1-1","7.0.9+~7.0.2-1","7.0.9+~7.0.2-2","8.0.11+~8.0.1-1","8.0.2+~7.0.11-1","8.0.3+~7.0.11-1","8.0.4+~7.0.11-1","8.0.4+~7.0.11-2","9.0.0+~8.0.1-1","9.0.3+~8.0.1-1","9.0.5+~8.0.1-1","9.0.6+~8.0.1-1"]},{"source":"osv","ecosystem":"Debian:13","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.0.0+~8.0.1-1","10.0.10+~8.0.1-1","6.10.0+~6.4.17-1","6.10.0+~6.4.17-1+deb13u1","7.0.10+~7.0.2-1","7.0.12+~7.0.5-1","7.0.5+~7.0.1-1","7.0.6+~7.0.1-1","7.0.9+~7.0.2-1","7.0.9+~7.0.2-2","8.0.11+~8.0.1-1","8.0.2+~7.0.11-1","8.0.3+~7.0.11-1","8.0.4+~7.0.11-1","8.0.4+~7.0.11-2","9.0.0+~8.0.1-1","9.0.3+~8.0.1-1","9.0.5+~8.0.1-1","9.0.6+~8.0.1-1"]},{"source":"osv","ecosystem":"Debian:14","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.0.0+~8.0.1-1"}]}],"versions":["6.10.0+~6.4.17-1","7.0.10+~7.0.2-1","7.0.12+~7.0.5-1","7.0.5+~7.0.1-1","7.0.6+~7.0.1-1","7.0.9+~7.0.2-1","7.0.9+~7.0.2-2","8.0.11+~8.0.1-1","8.0.2+~7.0.11-1","8.0.3+~7.0.11-1","8.0.4+~7.0.11-1","8.0.4+~7.0.11-2","9.0.0+~8.0.1-1","9.0.3+~8.0.1-1","9.0.5+~8.0.1-1","9.0.6+~8.0.1-1"]},{"source":"osv","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"8.0.9"}]}]},{"source":"osv","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]},{"source":"osv","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"SEMVER","events":[{"introduced":"6.9.16"},{"fixed":"9.1.0"}]}]},{"source":"osv","ecosystem":"npm","package":"nodemailer","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"6.10.1-aikido.10"},{"fixed":"6.10.1-aikido.11"},{"fixed":"7.0.11-aikido.6"},{"fixed":"7.0.11-aikido.7"},{"fixed":"6.10.1-aikido.12"},{"fixed":"7.0.11-aikido.5"},{"fixed":"6.10.1-aikido.9"},{"fixed":"6.10.1-aikido.8"},{"fixed":"6.10.1-aikido.5"},{"fixed":"7.0.11-aikido.3"},{"fixed":"7.0.9-aikido.6"},{"fixed":"6.10.1-aikido.6"},{"fixed":"7.0.11-aikido.4"},{"fixed":"7.0.9-aikido.7"},{"fixed":"7.0.9-aikido.8"},{"fixed":"6.10.1-aikido.13"},{"fixed":"7.0.11-aikido.8"},{"fixed":"7.0.11-aikido.9"},{"fixed":"6.10.1-aikido.14"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"@rootio/nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"6.10.1-root.io.10"},{"fixed":"6.10.1-root.io.11"},{"fixed":"7.0.11-root.io.6"},{"fixed":"7.0.11-root.io.7"},{"fixed":"6.10.1-root.io.12"},{"fixed":"7.0.11-root.io.5"},{"fixed":"6.10.1-root.io.9"},{"fixed":"6.10.1-root.io.8"},{"fixed":"6.10.1-root.io.5"},{"fixed":"7.0.11-root.io.3"},{"fixed":"7.0.9-root.io.6"},{"fixed":"6.10.1-root.io.6"},{"fixed":"7.0.11-root.io.4"},{"fixed":"7.0.9-root.io.7"},{"fixed":"7.0.9-root.io.8"},{"fixed":"6.10.1-root.io.13"},{"fixed":"7.0.11-root.io.8"},{"fixed":"7.0.11-root.io.9"},{"fixed":"6.10.1-root.io.14"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"6.10.1-aikido.10"},{"fixed":"9.0.5-aikido.2"},{"fixed":"6.10.1-aikido.11"},{"fixed":"7.0.11-aikido.6"},{"fixed":"7.0.11-aikido.7"},{"fixed":"7.0.13-aikido.3"},{"fixed":"9.0.1-aikido.2"},{"fixed":"9.0.3-aikido.3"},{"fixed":"7.0.13-aikido.4"},{"fixed":"9.0.1-aikido.3"},{"fixed":"9.0.5-aikido.3"},{"fixed":"9.0.3-aikido.2"},{"fixed":"6.10.1-aikido.12"},{"fixed":"8.0.10-aikido.1"},{"fixed":"8.0.5-aikido.3"},{"fixed":"8.0.5-aikido.2"},{"fixed":"9.0.4-aikido.1"},{"fixed":"8.0.10-aikido.2"},{"fixed":"7.0.9-aikido.8"},{"fixed":"9.0.4-aikido.2"},{"fixed":"8.0.10-aikido.3"},{"fixed":"8.0.5-aikido.4"},{"fixed":"9.0.5-aikido.4"},{"fixed":"7.0.13-aikido.5"},{"fixed":"9.0.1-aikido.4"},{"fixed":"6.10.1-aikido.13"},{"fixed":"9.0.3-aikido.4"},{"fixed":"7.0.11-aikido.8"},{"fixed":"7.0.11-aikido.9"},{"fixed":"7.0.13-aikido.6"},{"fixed":"7.0.13-aikido.7"},{"fixed":"6.10.1-aikido.14"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"@rootio/nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"6.10.1-root.io.10"},{"fixed":"9.0.5-root.io.2"},{"fixed":"6.10.1-root.io.11"},{"fixed":"8.0.10-root.io.1"},{"fixed":"7.0.11-root.io.6"},{"fixed":"7.0.11-root.io.7"},{"fixed":"7.0.13-root.io.3"},{"fixed":"9.0.1-root.io.2"},{"fixed":"9.0.3-root.io.3"},{"fixed":"7.0.13-root.io.4"},{"fixed":"9.0.1-root.io.3"},{"fixed":"9.0.5-root.io.3"},{"fixed":"9.0.3-root.io.2"},{"fixed":"6.10.1-root.io.12"},{"fixed":"8.0.5-root.io.3"},{"fixed":"8.0.5-root.io.2"},{"fixed":"9.0.4-root.io.1"},{"fixed":"8.0.10-root.io.2"},{"fixed":"7.0.9-root.io.8"},{"fixed":"9.0.4-root.io.2"},{"fixed":"8.0.10-root.io.3"},{"fixed":"8.0.5-root.io.4"},{"fixed":"9.0.5-root.io.4"},{"fixed":"7.0.13-root.io.5"},{"fixed":"9.0.1-root.io.4"},{"fixed":"6.10.1-root.io.13"},{"fixed":"9.0.3-root.io.4"},{"fixed":"7.0.11-root.io.8"},{"fixed":"7.0.11-root.io.9"},{"fixed":"7.0.13-root.io.6"},{"fixed":"7.0.13-root.io.7"},{"fixed":"6.10.1-root.io.14"}]}]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["6.3.0-2","6.3.1-1","6.4.0-1","6.4.1-1","6.4.2-1","6.4.5-1"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["6.4.17-3","6.7.0-1","6.7.0-2","6.7.1+~6.4.4-1"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["6.9.4+~6.4.9-2","6.9.4+~6.4.9-3ubuntu1","6.9.4+~6.4.9-4"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["8.0.4+~7.0.11-1","8.0.4+~7.0.11-2","9.0.0+~8.0.1-1","9.0.3+~8.0.1-1","9.0.5+~8.0.1-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"node-nodemailer","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["7.0.5+~7.0.1-1","7.0.9+~7.0.2-2","7.0.10+~7.0.2-1","7.0.12+~7.0.5-1","8.0.4+~7.0.11-1"]}],"products":[{"vendor":"nodemailer","product":"nodemailer"}],"references":[{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-2x7j-588g-ccc2","tags":["vendor-advisory","exploit","advisory","web","report"]},{"url":"https://github.com/nodemailer/nodemailer/commit/9116da9","tags":["patch","fix"]},{"url":"https://github.com/nodemailer/nodemailer/commit/7cc38af","tags":["patch","fix"]},{"url":"https://github.com/nodemailer/nodemailer/commit/34da642","tags":["patch","fix"]},{"url":"https://github.com/nodemailer/nodemailer/commit/83b8c48","tags":["patch","fix"]},{"url":"https://www.vulncheck.com/advisories/nodemailer-before-9.1.0-denial-of-service-via-addressparser","tags":["third-party-advisory","advisory"]},{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-268h-hp4c-crq3","tags":["web"]},{"url":"https://github.com/advisories/GHSA-268h-hp4c-crq3"},{"url":"https://github.com/nodemailer/nodemailer/pull/1848","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/34da64282dcdc9b0581c721a27ab2fa226673150","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/7cc38af418ffa6fc7e86085195ca5ca681694b3e","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/9116da9528c6524cefaed75185602a7e85d20434","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0","tags":["web"]},{"url":"https://github.com/advisories/GHSA-2x7j-588g-ccc2"},{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-cc9r-2j5m-2m83","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/902b63e935435c30f4025901c0902dce64cd8880","tags":["web"]},{"url":"https://github.com/advisories/GHSA-cc9r-2j5m-2m83"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-92596","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-v554-hhrr-96v5"},{"url":"https://github.com/nodemailer/nodemailer/security/advisories/GHSA-wmmp-3585-3rmp","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/259c32d7d266301e3377a212776c3fff993c0148","tags":["web"]},{"url":"https://github.com/nodemailer/nodemailer/commit/b212ac4e27bce8182478044fcb8d1642ccdad46e","tags":["web"]},{"url":"https://github.com/advisories/GHSA-wmmp-3585-3rmp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/92xxx/CVE-2026-92596.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-92596","tags":["advisory"]},{"url":"https://github.com/nodemailer/nodemailer","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-92596","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-92596","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-92596","title":"Nodemailer before 9.1.0 Denial of Service via addressparser","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T21:47:00.9Z","modified":"2026-09-17T13:23:33.704Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T13:48:24.536396Z"}],"euvd":[{"id":"EUVD-2026-81296","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T21:47:00Z","modified":"2026-09-17T13:23:33Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"updated_at":"2026-11-02T17:28:12.719968Z"}],"ghsa":[{"id":"GHSA-268h-hp4c-crq3","title":"Nodemailer: CRLF injection in Nodemailer List-* header comments allows arbitrary message header injection","description":"### Summary\n\nNodemailer constructs `List-*` headers from the caller-provided `list` message option using internally prepared header values. The `list.*.comment` field is inserted into those prepared values without removing CR (`\\r`) or LF (`\\n`) characters. Because prepared headers bypass the normal header-value sanitizer and are passed to `mimeFuncs.foldLines()`, a CRLF sequence in a list comment is emitted as an actual header boundary in the generated RFC822 message.\n\nAn application that lets a lower-privileged or unauthenticated user influence `list.help.comment`, `list.unsubscribe.comment`, `list.subscribe.comment`, `list.post.comment`, `list.owner.comment`, `list.archive.comment`, or `list.id.comment` can therefore be made to generate messages containing attacker-chosen additional headers.\n\n### Details\nSource-to-sink evidence:\n\n- `lib/mailer/mail-message.js:241-249` calls `_getListHeaders(this.data.list)` and adds each returned value with `this.message.addHeader(listHeader.key, value)`.\n- `lib/mailer/mail-message.js:253-296` builds each list header value as `{ prepared: true, foldLines: true, value: ... }`.\n- For `List-ID`, `lib/mailer/mail-message.js:272-279` copies `value.comment` into the generated header value. If `mimeFuncs.isPlainText(comment)` returns true, it wraps the comment in quotes rather than encoding or CRLF-normalizing it.\n- For the other `List-*` headers, `lib/mailer/mail-message.js:283-288` copies `value.comment` into `(\u003ccomment\u003e)`. If `mimeFuncs.isPlainText(comment)` returns true, the value is not encoded or CRLF-normalized.\n- `lib/mime-node/index.js:323-351` accepts the prepared header object.\n- `lib/mime-node/index.js:533-540` trusts `options.prepared`; when `foldLines` is set, it pushes `mimeFuncs.foldLines(key + ': ' + value)` directly into the header block.\n- The normal header-value sanitizer path is bypassed because the value is marked prepared. By contrast, ordinary unprepared header values are normalized in the regular header-building path.\n- `lib/mailer/mail-message.js:299-308` removes whitespace and angle brackets from `list.*.url`, so the confirmed injection source is the `comment` field, not the URL field.\n\nDefault/common exposure evidence:\n\n- `lib/nodemailer.js:21-60` exposes the public `createTransport(...).sendMail(...)` flow used by the package.\n- `examples/full.js:106-123` documents `list.unsubscribe.comment` and `list.id.comment` as normal message options.\n- The behavior is in shipped runtime code and does not require test-only code, non-default build steps, or undocumented internals.\n\nFalse-positive screening and negative controls:\n\n- SMTP command construction was separately reviewed. Envelope sender/recipients reject CRLF before SMTP commands, EHLO names strip CRLF, SIZE is numeric, and DSN fields are encoded; no SMTP command-injection variant was confirmed.\n- Ordinary `subject` header input containing CRLF was normalized to a single `Subject:` header and did not create `X-Injected` in the local control case.\n- Address display names and MIME filename/content-type parameters were reviewed by a focused MIME/header audit and were encoded or CRLF-normalized in local checks.\n- `prepared: true` custom headers are an explicit low-level escape hatch, but this issue is different because Nodemailer itself creates prepared headers from the documented `list.*.comment` option.\n\nVariant analysis:\n\nLocal testing confirmed the same root cause for comments in `List-Help`, `List-Unsubscribe`, `List-Subscribe`, `List-Post`, `List-Owner`, `List-Archive`, and `List-ID`. These should be fixed together by rejecting or normalizing CR/LF in list comments before prepared header generation, or by avoiding the prepared-header bypass for caller-controlled list values.\n\nAffected version evidence and uncertainty:\n\n- Confirmed vulnerable: `nodemailer` 8.0.8 at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`.\n- Git history shows `_getListHeaders` present in historical commits including `22fcff8` (`v4.3.0`) and related list-header work in `9b4f90a` (`v3.1.8`), but older versions were not dynamically tested during this audit.\n- Affected range is therefore recorded as unknown beyond the confirmed current version.\n- No patched version was identified in this checkout.\n\nSeverity rationale:\n\n- AV: The vulnerable library path is reached through application-level message submission in typical networked applications that use Nodemailer.\n- AC: A single CRLF sequence in a documented message option triggers the issue.\n- PR: Conservative assumption that the attacker is a lower-privileged user of an application that exposes list metadata fields. Some applications could expose this to unauthenticated users, but that was not assumed.\n- UI: No maintainer or victim interaction is needed after the application accepts the message object.\n- S: The impact remains in the application/mail-generation security scope.\n- C/I: Injected headers can affect message metadata, mail-client/filter interpretation, and downstream mail-pipeline decisions. No SMTP envelope recipient injection or code execution was demonstrated.\n- A: No availability impact was demonstrated.\n\nFinal self-review:\n\n- Reproduction evidence was generated locally from this checkout with a safe in-memory `streamTransport` PoC and a negative `Subject` control case.\n- The PoC is non-destructive and does not send network traffic outside the process.\n- The observed output contains an actual CRLF-delimited injected header line.\n- Reachability, sanitizer bypass, package exposure, variants, and non-exploitable sibling paths were checked as described above.\n- The affected range is not overclaimed; only the current tested version is confirmed vulnerable.\n\n### PoC\n\nFrom a clean checkout of `nodemailer` at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`, run:\n\n```bash\nnode \u003c\u003c'NODE'\n'use strict';\nconst nodemailer = require('./');\nconst headersEnd = raw =\u003e raw.slice(0, raw.indexOf('\\r\\n\\r\\n'));\nconst hasStandaloneInjected = raw =\u003e /\\r\\nX-Injected: yes\\)/.test(raw) || /\\r\\nX-Injected: yes\\r\\n/.test(raw);\n(async () =\u003e {\n  const transport = nodemailer.createTransport({ streamTransport: true, buffer: true });\n  const positive = await transport.sendMail({\n    from: 'sender@example.test',\n    to: 'recipient@example.test',\n    subject: 'control',\n    list: { unsubscribe: { url: 'https://example.test/u', comment: 'ok\\r\\nX-Injected: yes' } },\n    text: 'body'\n  });\n  const positiveRaw = positive.message.toString('utf8');\n  console.log('POSITIVE_HAS_INJECTED=' + hasStandaloneInjected(positiveRaw));\n  console.log('POSITIVE_LIST_LINE=' + JSON.stringify(headersEnd(positiveRaw).split('\\r\\n').filter(line =\u003e /^List-Unsubscribe:|^X-Injected:/.test(line)).join('\\n')));\n\n  const control = await transport.sendMail({\n    from: 'sender@example.test',\n    to: 'recipient@example.test',\n    subject: 'safe\\r\\nX-Injected: no',\n    text: 'body'\n  });\n  const controlRaw = control.message.toString('utf8');\n  console.log('CONTROL_HAS_INJECTED=' + /\\r\\nX-Injected: no\\r\\n/.test(controlRaw));\n  console.log('CONTROL_SUBJECT=' + JSON.stringify(headersEnd(controlRaw).split('\\r\\n').filter(line =\u003e /^Subject:|^X-Injected:/.test(line)).join('\\n')));\n\n  const variantKeys = ['help', 'unsubscribe', 'subscribe', 'post', 'owner', 'archive', 'id'];\n  const result = [];\n  for (const key of variantKeys) {\n    const info = await transport.sendMail({\n      from: 'sender@example.test',\n      to: 'recipient@example.test',\n      subject: 'variant ' + key,\n      list: Object.assign({}, { [key]: { url: key === 'id' ? 'example.test' : 'https://example.test/' + key, comment: 'c\\r\\nX-Variant-' + key + ': yes' } }),\n      text: 'body'\n    });\n    result.push(key + '=' + new RegExp('\\\\r\\\\nX-Variant-' + key + ': yes').test(info.message.toString('utf8')));\n  }\n  console.log('VARIANTS=' + result.join(','));\n})().catch(err =\u003e { console.error(err \u0026\u0026 err.stack || err); process.exit(1); });\nNODE\n```\n\nObserved output in this environment:\n\n```text\nPOSITIVE_HAS_INJECTED=true\nPOSITIVE_LIST_LINE=\"List-Unsubscribe: \u003chttps://example.test/u\u003e (ok\\nX-Injected: yes)\"\nCONTROL_HAS_INJECTED=false\nCONTROL_SUBJECT=\"Subject: safe X-Injected: no\"\nVARIANTS=help=true,unsubscribe=true,subscribe=true,post=true,owner=true,archive=true,id=true\n```\n\nExpected vulnerable output: `POSITIVE_HAS_INJECTED=true` and all listed variants ending in `=true`. Expected negative/control output: `CONTROL_HAS_INJECTED=false`, showing the ordinary `Subject` header path does not create a separate injected header.\n\nCleanup: none required; the PoC uses only in-memory message generation.\n\n### Impact\n\nA lower-privileged attacker who can influence `list.*.comment` fields in an application using Nodemailer can inject arbitrary additional headers into generated email messages. This can alter message semantics and downstream mail-client or mail-filter behavior, including adding attacker-controlled metadata headers. The PoC confirms header-boundary injection in the generated RFC822 output; it does not demonstrate SMTP command injection, recipient injection, or code execution.\n\n### Suggested remediation\n\nNormalize or reject CR and LF in `list.*.comment` before constructing prepared `List-*` headers. Prefer sharing the same CRLF-neutralization behavior used for ordinary header values, or avoid using `prepared: true` for caller-controlled list comment content. Add regression tests for CRLF in every documented `list` comment-bearing field and verify that generated messages do not contain attacker-controlled standalone headers.","status":"published","published":"2026-06-15T17:36:06Z","modified":"2026-06-15T17:36:07Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-93"],"updated_at":"2026-11-02T14:40:27.329706Z"},{"id":"GHSA-2x7j-588g-ccc2","title":"Nodemailer: Quadratic (O(n²)) time complexity in addressparser allows remote denial of service via a crafted address list","description":"### Summary\n\nNodemailer'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. a `To`, `Cc`, `Bcc`, `From`, or `Reply‑To` value, or any value passed to the exported `addressparser`) 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.\n\nThis 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).\n\n### Details\n\n`addressparser` tokenizes 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):\n\n```js\naddresses.forEach(addr =\u003e {\n    const handled = _handleAddress(addr, depth);\n    if (handled.length) {\n        parsedAddresses = parsedAddresses.concat(handled);   // \u003c-- line ~503\n    }\n});\n```\n\n`Array.prototype.concat` builds and returns a **new** array containing a copy of every element accumulated so far. Reassigning `parsedAddresses = 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 `_handleAddress` themselves are linear; the quadratic blowup is entirely this accumulator.\n\n**Root‑cause proof.** Replacing only that line with an in‑place append and re‑running the exact same input:\n\n```\nparsedAddresses = parsedAddresses.concat(handled);      -\u003e  100000 addresses:  ~6068 ms\nparsedAddresses.push.apply(parsedAddresses, handled);   -\u003e  100000 addresses:  ~51 ms   (≈119x faster, now linear)\n```\n\n**Measured scaling** (nodemailer 9.0.6, `'a@b.com,'.repeat(n)`):\n\n| addresses n | input size | parse time | ratio for 2× input |\n|---|---|---|---|\n| 25,000  | 0.19 MB | ~0.35 s | – |\n| 50,000  | 0.38 MB | ~1.4 s  | ×4.0 |\n| 100,000 | 0.76 MB | ~6–8 s  | ×3.9 |\n| 200,000 | 1.53 MB | ~25–30 s| ×4.1 |\n\nDoubling the input quadruples the time — the signature of O(n²).\n\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`, and `getEnvelope()`), so a single `transport.sendMail({ to: \u003ccrafted string\u003e })` triggers it. It is also reached directly through the **exported** `require('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.\n\n**Suggested fix:** accumulate in place instead of rebuilding the array each iteration, e.g. `parsedAddresses.push.apply(parsedAddresses, handled);` (or `for (const h of handled) parsedAddresses.push(h);`). Optionally cap the number of addresses / input length before parsing.\n\n### PoC\n\nEnvironment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No transport, network, or configuration required — the cost is in parsing.\n\n`poc-dos.js`:\n```js\n'use strict';\nconst addressparser = require('nodemailer/lib/addressparser');\n\nconsole.log('addresses | input size | parse time');\nfor (const n of [25000, 50000, 100000, 200000]) {\n  const payload = 'a@b.com,'.repeat(n);        // n valid, comma-separated recipients\n  const t0 = process.hrtime.bigint();\n  addressparser(payload);                       // blocks synchronously\n  const ms = Number(process.hrtime.bigint() - t0) / 1e6;\n  console.log(String(n).padStart(9) + ' | ' + (payload.length / 1048576).toFixed(2) + ' MB   | ' + ms.toFixed(0).padStart(7) + ' ms');\n}\n```\n\nRun:\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-dos.js\n```\n\nActual output (nodemailer 9.0.6):\n```\naddresses | input size | parse time\n    25000 | 0.19 MB   |     381 ms\n    50000 | 0.38 MB   |    1435 ms\n   100000 | 0.76 MB   |    7949 ms\n   200000 | 1.53 MB   |   25154 ms\n```\n\nEquivalent trigger through the normal send API (freezes the event loop):\n```js\nconst nodemailer = require('nodemailer');\nnodemailer.createTransport({ jsonTransport: true })\n  .sendMail({ from: 'a@b.com', to: 'a@b.com,'.repeat(150000), subject: 'x', text: 'y' });\n// ~15+ seconds of 100% CPU inside addressparser before anything is sent\n```\n\n### Impact\n\n* **Who is impacted:** any service that runs Nodemailer (or the standalone `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, a `Reply‑To`/`From` derived from user input, a contact‑import or mailing‑list parser, or any endpoint that validates addresses with `addressparser`. No authentication, special option, or particular receiver is needed.\n\n## Patched in 9.1.0\n\nThree separate quadratic paths were fixed, not one:\n\n* `addressparser` rebuilt its accumulator with `concat()` on every address ([9116da9](https://github.com/nodemailer/nodemailer/commit/9116da9)).\n* The display-name merge loop directly below spliced each fragment out of the array, the same shape reached through `'a, b \u003cc@d.com\u003e,'.repeat(n)` (same commit).\n* `MimeNode#_convertAddresses` checked recipient uniqueness with a linear scan per address ([7cc38af](https://github.com/nodemailer/nodemailer/commit/7cc38af), refined in [34da642](https://github.com/nodemailer/nodemailer/commit/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 after `addressparser` was fixed.\n\nFixed alongside: `[].concat.apply` in `_parseAddresses` threw `RangeError: Maximum call stack size exceeded` past roughly 124k recipients, with no crafted input needed ([83b8c48](https://github.com/nodemailer/nodemailer/commit/83b8c48)).\n\nParsing 200k addresses now takes ~80ms instead of ~25s, and every path scales linearly. A new `maxRecipients` option (default 100000) throws rather than truncating, as a backstop.","status":"published","published":"2026-09-08T21:33:17Z","modified":"2026-09-08T21:33:19Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400","CWE-407"],"updated_at":"2026-11-02T14:41:14.764963Z"},{"id":"GHSA-cc9r-2j5m-2m83","title":"Nodemailer: Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain","description":"### Summary\n\nNodemailer'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 as `user@good-corp.com(x)evil.com` is parsed and **delivered to `good-corp.comevil.com`** (registrable domain `comevil.com`, attacker‑controlled), while a conformant RFC 5322 parser terminates the domain at the comment and reads `good-corp.com`.\n\nAn 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).\n\nSeverity 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`).\n\n### Details\n\nRoot cause is in `lib/addressparser/index.js`.\n\n1. The tokenizer registers the comment as an operator pair (`Tokenizer.operators`):\n   ```js\n   '(': ')',            // line ~331\n   ```\n2. When the **closing** `)` is immediately followed by a non‑break character (anything other than space / tab / CR / LF / `,` / `;`), the tokenizer marks that operator token with `noBreak = true`:\n   ```js\n   // Tokenizer.checkChar, lines ~398-399\n   if (nextChr \u0026\u0026 ![' ', '\\t', '\\r', '\\n', ',', ';'].includes(nextChr)) {\n       this.node.noBreak = true;\n   }\n   ```\n3. `_handleAddress` then **glues** the token that follows the comment onto the token that preceded it (dropping the comment):\n   ```js\n   // _handleAddress, lines ~187-188\n   if (prevToken \u0026\u0026 prevToken.noBreak \u0026\u0026 data[state].length) {\n       data[state][data[state].length - 1] += token.value;   // \u003c-- concatenation\n   }\n   ```\n\nFor the input `user@good-corp.com(x)evil.com` the tokens are `text:\"user@good-corp.com\"`, `op:\"(\"`, `text:\"x\"`, `op:\")\"` (flagged `noBreak`), `text:\"evil.com\"`. Step 3 appends `evil.com` onto `user@good-corp.com`, producing the single domain **`good-corp.comevil.com`**. The comment content (`x`) is discarded into the display‑name field.\n\nRFC 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 of `good-corp.com(x)evil.com` is the domain `good-corp.com` (with the trailing `evil.com` being invalid/ignored). Nodemailer instead concatenates the two atoms across the removed comment, yielding a different, attacker‑registrable domain.\n\nNodemailer uses the parsed address for **both** the SMTP envelope (`getEnvelope()` → `RCPT TO`) and the emitted `To:`/`From:` headers, so the entire message is routed to the concatenated domain.\n\n**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 as `user@x.com(a(b)c)` is mis‑balanced and mangled to `x.comc)`. That particular output contains a stray `)` and is **rejected** by a conformant MTA (501) — a bounce/robustness issue, not a misroute.\n\n**Suggested fix:** treat a comment as folding whitespace that terminates the current token — i.e. do **not** propagate `noBreak` across a comment‑closing `)` (restrict the `noBreak` optimization 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.\n\n### PoC\n\nEnvironment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No special transport configuration is required; the discrepancy is in address parsing.\n\n`poc-comment.js`:\n```js\n'use strict';\nconst net = require('net');\nconst nodemailer = require('nodemailer'); // 9.0.6\n\nconst TRUSTED   = 'good-corp.com';\nconst RECIPIENT = 'user@good-corp.com(x)evil.com'; // RFC 5322 comment (x) between two domains\n\n// tiny SMTP sink that prints the literal RCPT TO nodemailer transmits\nconst server = net.createServer(sock =\u003e {\n  let buf = ''; sock.write('220 sink\\r\\n');\n  sock.on('data', d =\u003e { buf += d; let i;\n    while ((i = buf.indexOf('\\r\\n')) \u003e= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);\n      const u = line.toUpperCase();\n      if (u.startsWith('EHLO')) sock.write('250-sink\\r\\n250 8BITMIME\\r\\n');\n      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits :', line); sock.write('250 ok\\r\\n'); }\n      else if (u.startsWith('DATA')) sock.write('354 go\\r\\n');\n      else if (line === '.') sock.write('250 ok\\r\\n');\n      else if (u.startsWith('QUIT')) { sock.write('221 bye\\r\\n'); sock.end(); }\n      else sock.write('250 ok\\r\\n'); } });\n});\nserver.listen(0, '127.0.0.1', async () =\u003e {\n  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });\n  await t.sendMail({ from: 'app@good-corp.com', to: RECIPIENT, subject: 'hi', text: 'x' });\n  t.close(); server.close();\n});\n```\n\nRun:\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-comment.js\n```\n\nActual output (nodemailer 9.0.6):\n```\nnodemailer transmits : RCPT TO:\u003cuser@good-corp.comevil.com\u003e\n```\n\nThe application asked to mail `user@good-corp.com(x)evil.com`; Nodemailer delivers to `good-corp.comevil.com` — registrable domain `comevil.com`, which an attacker can register.\n\n**Verified against a real RFC 5321 server** (containerized lab included with this report — `docker compose up --build`, case `R8_comment_glue`, receiver = `aiosmtpd`):\n```\nwire RCPT TO                     : RCPT TO:\u003cuser@good-corp.comevil.com\u003e\nreal server                      : ACCEPTED (250)\nrecipient parsed by real server  : user@good-corp.comevil.com   (domain good-corp.comevil.com)\ndelivered To header              : x \u003cuser@good-corp.comevil.com\u003e\n```\n\n**Which parser sees what** (the crux of exploitability):\n\n| Parser used by the application to gate/route | Domain it reads from `user@good-corp.com(x)evil.com` | Deceived? |\n|---|---|---|\n| Python `email.policy.default` (strict RFC 5322) | `good-corp.com` *(flags `InvalidHeaderDefect`)* | Yes, if defects are not checked |\n| Naive prefix / substring allow‑list (`startsWith`/`includes('@good-corp.com')`) | `good-corp.com` | Yes |\n| Nodemailer's own `addressparser` | `good-corp.comevil.com` | No |\n| Python `email.utils.getaddresses` | `good-corp.comevil.com` | No |\n| WHATWG `url.domainToASCII` | `good-corp.com(x)evil.com` | No |\n\n### Impact\n\n\n\n* **Who is impacted:** applications that make a security or routing decision on the recipient **domain** using a parser that terminates the domain at the comment, while relying on Nodemailer for delivery — specifically those that validate with a strict RFC 5322 parser **without inspecting parse defects**, or with a **prefix/substring/allow‑list** check (e.g. \"only send to `@good-corp.com`\", employee‑only flows, \"same‑tenant\" routing). Applications that validate with Nodemailer's own `addressparser`, `email.utils.getaddresses`, or `url.domainToASCII` are **not** affected, which is why this is rated below the IDN/Punycode issue.\n\n## Patched in 9.1.0\n\nFixed in [902b63e](https://github.com/nodemailer/nodemailer/commit/902b63e).\n\nNot propagating `noBreak` across the closing `)` on its own breaks valid addresses, because CFWS is legal on either side of the `@`: `user@(x)good-corp.com` and `user(x)@good-corp.com` both come out mangled. A comment now joins what it separates only when one side carries the `@`, so those keep resolving while `user@good-corp.com(x)evil.com` terminates at `good-corp.com`.\n\nQuoted-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 yields `user@x.com`.","status":"published","published":"2026-09-08T21:32:51Z","modified":"2026-09-08T21:32:53Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-20","CWE-436"],"updated_at":"2026-11-02T14:41:14.764963Z"},{"id":"GHSA-v554-hhrr-96v5","title":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser...","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-17T00:31:27Z","modified":"2026-09-17T00:31:32Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T15:12:03.458644Z"},{"id":"GHSA-wmmp-3585-3rmp","title":"Nodemailer: IDN/Punycode domain allow-list bypass leads to email delivery to an attacker-controlled domain","description":"### Summary\n\nNodemailer 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's `url.domainToASCII`, Python's `idna`). Its address normalizer (`_normalizeAddress` in `lib/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**.\n\nAn 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.\n\nBecause 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.\n\n### Details\n\n`lib/mime-node/index.js` → `_normalizeAddress(address)` (around lines 1307–1346) splits the address at the last `@` and normalizes the domain like this:\n\n```js\n// lib/mime-node/index.js\ntry {\n    if (/[\\x80-￿]/.test(user)) {\n        encodedDomain = punycode.toUnicode(domain.toLowerCase());   // line ~1338\n    } else {\n        encodedDomain = punycode.toASCII(domain.toLowerCase());     // line ~1340\n    }\n} catch (_err) {\n    // keep domain as supplied\n}\nreturn `${this._normalizeLocalPart(user)}@${encodedDomain}`;         // line ~1346\n```\n\n`punycode` here 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:\n\n* removing **Ignored** code points such as `U+00AD` SOFT HYPHEN,\n* **Mapping** full‑width / compatibility characters to their canonical ASCII forms,\n* Unicode **NFC** normalization,\n* validity checks.\n\nAs a result, for any domain containing a UTS‑46‑mapped or ‑ignored character, Nodemailer’s `punycode.toASCII(...)` produces a **different A‑label** than `url.domainToASCII(...)` (Node ≥ 7 / WHATWG), `new URL('http://'+domain)`, browsers, and Python’s `idna` (`uts46=True`). Nodemailer then uses its A‑label as:\n\n* the SMTP envelope recipient written to the wire as `RCPT TO:\u003clocal@xn--…\u003e` (`getEnvelope()` → `lib/smtp-connection/index.js` `_setEnvelope`), **and**\n* the address emitted in the `To:` / `From:` headers (`_convertAddresses`).\n\nSo 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):\n\n| recipient (raw) | UTS‑46 parser (`url.domainToASCII`) | Nodemailer delivers to |\n|---|---|---|\n| `victim@compa{U+00AD}ny.com` (invisible soft hyphen) | `company.com` | `xn--company-pka.com` |\n| `victim@ｃｏｍｐａｎｙ.com` (full‑width) | `company.com` | `xn--mi7cd4afch9d.com` |\n| `user@exámple.com` (NFD `a`+U+0301) | `xn--exmple-qta.com` | `xn--example-vge.com` |\n\nThis 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_convert` library). 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.\n\n**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 raw `punycode.toASCII`, and decode with the matching UTS‑46 `domainToUnicode`. At minimum, reject a domain whose value changes under UTS‑46 mapping (i.e. `punycode.toASCII(d)` ≠ `url.domainToASCII(d)`).\n\n### PoC\n\nEnvironment: Node.js ≥ 18, the published `nodemailer@9.0.6`. No special configuration; the discrepancy is in domain normalization itself.\n\n`poc-idn.js`:\n\n```js\n'use strict';\nconst net = require('net');\nconst url = require('url');\nconst nodemailer = require('nodemailer'); // 9.0.6\n\nconst TRUSTED   = 'company.com';                        // the only domain the app will mail\nconst RECIPIENT = 'victim@compa\\u00ADny.com';           // attacker input: invisible U+00AD inside \"company\"\n\n// The app's domain allow-list check, done the standard (UTS-46 / browser / WHATWG) way:\nconst seen = url.domainToASCII(RECIPIENT.split('@').pop());\nconsole.log('validator (url.domainToASCII) sees:', JSON.stringify(seen),\n            seen === TRUSTED ? '=\u003e ALLOWED (equals trusted domain)' : '');\n\n// A tiny SMTP sink that prints the literal RCPT TO Nodemailer transmits:\nconst server = net.createServer(sock =\u003e {\n  let buf = ''; sock.write('220 sink\\r\\n');\n  sock.on('data', d =\u003e { buf += d; let i;\n    while ((i = buf.indexOf('\\r\\n')) \u003e= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);\n      const u = line.toUpperCase();\n      if (u.startsWith('EHLO')) sock.write('250-sink\\r\\n250 8BITMIME\\r\\n');\n      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits             :', line); sock.write('250 ok\\r\\n'); }\n      else if (u.startsWith('DATA')) sock.write('354 go\\r\\n');\n      else if (line === '.') sock.write('250 ok\\r\\n');\n      else if (u.startsWith('QUIT')) { sock.write('221 bye\\r\\n'); sock.end(); }\n      else sock.write('250 ok\\r\\n'); } });\n});\nserver.listen(0, '127.0.0.1', async () =\u003e {\n  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });\n  await t.sendMail({ from: 'app@company.com', to: RECIPIENT, subject: 'reset your password', text: 'secret link' });\n  t.close(); server.close();\n});\n```\n\nRun:\n\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-idn.js\n```\n\nActual output (Nodemailer 9.0.6):\n\n```\nvalidator (url.domainToASCII) sees: \"company.com\" =\u003e ALLOWED (equals trusted domain)\nnodemailer transmits             : RCPT TO:\u003cvictim@xn--company-pka.com\u003e\n```\n\nThe application’s domain check approves `company.com`, but the message is sent to `xn--company-pka.com` — a **different domain an attacker can register** — carrying the `To:` header `\u003cvictim@xn--company-pka.com\u003e` as well.\n\nA containerized version that proves the same result against a **real RFC 5321 SMTP server** (`aiosmtpd`) is included alongside this report (`docker compose up --build`, cases `R6`/IDN); the receiving server accepts `RCPT TO:\u003cvictim@xn--company-pka.com\u003e` and reports the recipient domain as `xn--company-pka.com`.\n\n### Impact\n\nAny 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:\n  * recipient **allow‑list / block‑list / “same corporate domain” checks** implemented with `new URL()`, `url.domainToASCII`, a browser‑side check, or an IDN library;\n  * flows that **display or log the normalized recipient domain** for human confirmation (the shown `company.com` differs from the delivered `xn--company-pka.com`);\n  * any domain‑gated feature (employee‑only registration, “send only to our tenant”, notification routing).\n\n## Patched in 9.1.0\n\nDomain encoding now applies UTS-46 ([259c32d](https://github.com/nodemailer/nodemailer/commit/259c32d)), so `victim@compa­ny.com` resolves to `company.com`, matching `url.domainToASCII` and browsers.\n\nOne caveat on the suggested remediation, hardened in [b212ac4](https://github.com/nodemailer/nodemailer/commit/b212ac4): `url.domainToASCII` is 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, since `user@attacker.example/mail.corp.example` encodes to the deliverable `user@attacker.example` where the bundled Punycode codec left it intact and unroutable. Those characters are now kept away from the mapper.\n\nOn severity, \"attacker-registrable\" is doing significant work in the report: `xn--company-pka.com` decodes to a label containing U+00AD and `xn--mi7cd4afch9d.com` to 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.","status":"published","published":"2026-09-08T21:33:32Z","modified":"2026-09-08T21:33:33Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-20","CWE-436"],"updated_at":"2026-11-02T14:41:14.764963Z"}],"nvd":[{"id":"CVE-2026-92596","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T22:18:30.557Z","modified":"2026-09-22T20:25:55.87Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T14:12:49.013904Z"}],"osv":[{"id":"CVE-2026-92596","title":"Nodemailer before 9.1.0 Denial of Service via addressparser","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T21:47:00.9Z","modified":"2026-10-28T02:30:50.78828463Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T15:45:20.588039Z"},{"id":"DEBIAN-CVE-2026-92596","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T22:18:30.557Z","modified":"2026-09-23T08:47:28.491922169Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high"}],"updated_at":"2026-11-02T15:46:06.846102Z"},{"id":"GHSA-268h-hp4c-crq3","title":"Nodemailer: CRLF injection in Nodemailer List-* header comments allows arbitrary message header injection","description":"### Summary\n\nNodemailer constructs `List-*` headers from the caller-provided `list` message option using internally prepared header values. The `list.*.comment` field is inserted into those prepared values without removing CR (`\\r`) or LF (`\\n`) characters. Because prepared headers bypass the normal header-value sanitizer and are passed to `mimeFuncs.foldLines()`, a CRLF sequence in a list comment is emitted as an actual header boundary in the generated RFC822 message.\n\nAn application that lets a lower-privileged or unauthenticated user influence `list.help.comment`, `list.unsubscribe.comment`, `list.subscribe.comment`, `list.post.comment`, `list.owner.comment`, `list.archive.comment`, or `list.id.comment` can therefore be made to generate messages containing attacker-chosen additional headers.\n\n### Details\nSource-to-sink evidence:\n\n- `lib/mailer/mail-message.js:241-249` calls `_getListHeaders(this.data.list)` and adds each returned value with `this.message.addHeader(listHeader.key, value)`.\n- `lib/mailer/mail-message.js:253-296` builds each list header value as `{ prepared: true, foldLines: true, value: ... }`.\n- For `List-ID`, `lib/mailer/mail-message.js:272-279` copies `value.comment` into the generated header value. If `mimeFuncs.isPlainText(comment)` returns true, it wraps the comment in quotes rather than encoding or CRLF-normalizing it.\n- For the other `List-*` headers, `lib/mailer/mail-message.js:283-288` copies `value.comment` into `(\u003ccomment\u003e)`. If `mimeFuncs.isPlainText(comment)` returns true, the value is not encoded or CRLF-normalized.\n- `lib/mime-node/index.js:323-351` accepts the prepared header object.\n- `lib/mime-node/index.js:533-540` trusts `options.prepared`; when `foldLines` is set, it pushes `mimeFuncs.foldLines(key + ': ' + value)` directly into the header block.\n- The normal header-value sanitizer path is bypassed because the value is marked prepared. By contrast, ordinary unprepared header values are normalized in the regular header-building path.\n- `lib/mailer/mail-message.js:299-308` removes whitespace and angle brackets from `list.*.url`, so the confirmed injection source is the `comment` field, not the URL field.\n\nDefault/common exposure evidence:\n\n- `lib/nodemailer.js:21-60` exposes the public `createTransport(...).sendMail(...)` flow used by the package.\n- `examples/full.js:106-123` documents `list.unsubscribe.comment` and `list.id.comment` as normal message options.\n- The behavior is in shipped runtime code and does not require test-only code, non-default build steps, or undocumented internals.\n\nFalse-positive screening and negative controls:\n\n- SMTP command construction was separately reviewed. Envelope sender/recipients reject CRLF before SMTP commands, EHLO names strip CRLF, SIZE is numeric, and DSN fields are encoded; no SMTP command-injection variant was confirmed.\n- Ordinary `subject` header input containing CRLF was normalized to a single `Subject:` header and did not create `X-Injected` in the local control case.\n- Address display names and MIME filename/content-type parameters were reviewed by a focused MIME/header audit and were encoded or CRLF-normalized in local checks.\n- `prepared: true` custom headers are an explicit low-level escape hatch, but this issue is different because Nodemailer itself creates prepared headers from the documented `list.*.comment` option.\n\nVariant analysis:\n\nLocal testing confirmed the same root cause for comments in `List-Help`, `List-Unsubscribe`, `List-Subscribe`, `List-Post`, `List-Owner`, `List-Archive`, and `List-ID`. These should be fixed together by rejecting or normalizing CR/LF in list comments before prepared header generation, or by avoiding the prepared-header bypass for caller-controlled list values.\n\nAffected version evidence and uncertainty:\n\n- Confirmed vulnerable: `nodemailer` 8.0.8 at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`.\n- Git history shows `_getListHeaders` present in historical commits including `22fcff8` (`v4.3.0`) and related list-header work in `9b4f90a` (`v3.1.8`), but older versions were not dynamically tested during this audit.\n- Affected range is therefore recorded as unknown beyond the confirmed current version.\n- No patched version was identified in this checkout.\n\nSeverity rationale:\n\n- AV: The vulnerable library path is reached through application-level message submission in typical networked applications that use Nodemailer.\n- AC: A single CRLF sequence in a documented message option triggers the issue.\n- PR: Conservative assumption that the attacker is a lower-privileged user of an application that exposes list metadata fields. Some applications could expose this to unauthenticated users, but that was not assumed.\n- UI: No maintainer or victim interaction is needed after the application accepts the message object.\n- S: The impact remains in the application/mail-generation security scope.\n- C/I: Injected headers can affect message metadata, mail-client/filter interpretation, and downstream mail-pipeline decisions. No SMTP envelope recipient injection or code execution was demonstrated.\n- A: No availability impact was demonstrated.\n\nFinal self-review:\n\n- Reproduction evidence was generated locally from this checkout with a safe in-memory `streamTransport` PoC and a negative `Subject` control case.\n- The PoC is non-destructive and does not send network traffic outside the process.\n- The observed output contains an actual CRLF-delimited injected header line.\n- Reachability, sanitizer bypass, package exposure, variants, and non-exploitable sibling paths were checked as described above.\n- The affected range is not overclaimed; only the current tested version is confirmed vulnerable.\n\n### PoC\n\nFrom a clean checkout of `nodemailer` at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`, run:\n\n```bash\nnode \u003c\u003c'NODE'\n'use strict';\nconst nodemailer = require('./');\nconst headersEnd = raw =\u003e raw.slice(0, raw.indexOf('\\r\\n\\r\\n'));\nconst hasStandaloneInjected = raw =\u003e /\\r\\nX-Injected: yes\\)/.test(raw) || /\\r\\nX-Injected: yes\\r\\n/.test(raw);\n(async () =\u003e {\n  const transport = nodemailer.createTransport({ streamTransport: true, buffer: true });\n  const positive = await transport.sendMail({\n    from: 'sender@example.test',\n    to: 'recipient@example.test',\n    subject: 'control',\n    list: { unsubscribe: { url: 'https://example.test/u', comment: 'ok\\r\\nX-Injected: yes' } },\n    text: 'body'\n  });\n  const positiveRaw = positive.message.toString('utf8');\n  console.log('POSITIVE_HAS_INJECTED=' + hasStandaloneInjected(positiveRaw));\n  console.log('POSITIVE_LIST_LINE=' + JSON.stringify(headersEnd(positiveRaw).split('\\r\\n').filter(line =\u003e /^List-Unsubscribe:|^X-Injected:/.test(line)).join('\\n')));\n\n  const control = await transport.sendMail({\n    from: 'sender@example.test',\n    to: 'recipient@example.test',\n    subject: 'safe\\r\\nX-Injected: no',\n    text: 'body'\n  });\n  const controlRaw = control.message.toString('utf8');\n  console.log('CONTROL_HAS_INJECTED=' + /\\r\\nX-Injected: no\\r\\n/.test(controlRaw));\n  console.log('CONTROL_SUBJECT=' + JSON.stringify(headersEnd(controlRaw).split('\\r\\n').filter(line =\u003e /^Subject:|^X-Injected:/.test(line)).join('\\n')));\n\n  const variantKeys = ['help', 'unsubscribe', 'subscribe', 'post', 'owner', 'archive', 'id'];\n  const result = [];\n  for (const key of variantKeys) {\n    const info = await transport.sendMail({\n      from: 'sender@example.test',\n      to: 'recipient@example.test',\n      subject: 'variant ' + key,\n      list: Object.assign({}, { [key]: { url: key === 'id' ? 'example.test' : 'https://example.test/' + key, comment: 'c\\r\\nX-Variant-' + key + ': yes' } }),\n      text: 'body'\n    });\n    result.push(key + '=' + new RegExp('\\\\r\\\\nX-Variant-' + key + ': yes').test(info.message.toString('utf8')));\n  }\n  console.log('VARIANTS=' + result.join(','));\n})().catch(err =\u003e { console.error(err \u0026\u0026 err.stack || err); process.exit(1); });\nNODE\n```\n\nObserved output in this environment:\n\n```text\nPOSITIVE_HAS_INJECTED=true\nPOSITIVE_LIST_LINE=\"List-Unsubscribe: \u003chttps://example.test/u\u003e (ok\\nX-Injected: yes)\"\nCONTROL_HAS_INJECTED=false\nCONTROL_SUBJECT=\"Subject: safe X-Injected: no\"\nVARIANTS=help=true,unsubscribe=true,subscribe=true,post=true,owner=true,archive=true,id=true\n```\n\nExpected vulnerable output: `POSITIVE_HAS_INJECTED=true` and all listed variants ending in `=true`. Expected negative/control output: `CONTROL_HAS_INJECTED=false`, showing the ordinary `Subject` header path does not create a separate injected header.\n\nCleanup: none required; the PoC uses only in-memory message generation.\n\n### Impact\n\nA lower-privileged attacker who can influence `list.*.comment` fields in an application using Nodemailer can inject arbitrary additional headers into generated email messages. This can alter message semantics and downstream mail-client or mail-filter behavior, including adding attacker-controlled metadata headers. The PoC confirms header-boundary injection in the generated RFC822 output; it does not demonstrate SMTP command injection, recipient injection, or code execution.\n\n### Suggested remediation\n\nNormalize or reject CR and LF in `list.*.comment` before constructing prepared `List-*` headers. Prefer sharing the same CRLF-neutralization behavior used for ordinary header values, or avoid using `prepared: true` for caller-controlled list comment content. Add regression tests for CRLF in every documented `list` comment-bearing field and verify that generated messages do not contain attacker-controlled standalone headers.","status":"published","published":"2026-06-15T17:36:06Z","modified":"2026-09-10T03:51:07.307465115Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-93"],"updated_at":"2026-11-02T15:46:33.956911Z"},{"id":"GHSA-2x7j-588g-ccc2","title":"Nodemailer: Quadratic (O(n²)) time complexity in addressparser allows remote denial of service via a crafted address list","description":"### Summary\n\nNodemailer'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. a `To`, `Cc`, `Bcc`, `From`, or `Reply‑To` value, or any value passed to the exported `addressparser`) 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.\n\nThis 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).\n\n### Details\n\n`addressparser` tokenizes 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):\n\n```js\naddresses.forEach(addr =\u003e {\n    const handled = _handleAddress(addr, depth);\n    if (handled.length) {\n        parsedAddresses = parsedAddresses.concat(handled);   // \u003c-- line ~503\n    }\n});\n```\n\n`Array.prototype.concat` builds and returns a **new** array containing a copy of every element accumulated so far. Reassigning `parsedAddresses = 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 `_handleAddress` themselves are linear; the quadratic blowup is entirely this accumulator.\n\n**Root‑cause proof.** Replacing only that line with an in‑place append and re‑running the exact same input:\n\n```\nparsedAddresses = parsedAddresses.concat(handled);      -\u003e  100000 addresses:  ~6068 ms\nparsedAddresses.push.apply(parsedAddresses, handled);   -\u003e  100000 addresses:  ~51 ms   (≈119x faster, now linear)\n```\n\n**Measured scaling** (nodemailer 9.0.6, `'a@b.com,'.repeat(n)`):\n\n| addresses n | input size | parse time | ratio for 2× input |\n|---|---|---|---|\n| 25,000  | 0.19 MB | ~0.35 s | – |\n| 50,000  | 0.38 MB | ~1.4 s  | ×4.0 |\n| 100,000 | 0.76 MB | ~6–8 s  | ×3.9 |\n| 200,000 | 1.53 MB | ~25–30 s| ×4.1 |\n\nDoubling the input quadruples the time — the signature of O(n²).\n\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`, and `getEnvelope()`), so a single `transport.sendMail({ to: \u003ccrafted string\u003e })` triggers it. It is also reached directly through the **exported** `require('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.\n\n**Suggested fix:** accumulate in place instead of rebuilding the array each iteration, e.g. `parsedAddresses.push.apply(parsedAddresses, handled);` (or `for (const h of handled) parsedAddresses.push(h);`). Optionally cap the number of addresses / input length before parsing.\n\n### PoC\n\nEnvironment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No transport, network, or configuration required — the cost is in parsing.\n\n`poc-dos.js`:\n```js\n'use strict';\nconst addressparser = require('nodemailer/lib/addressparser');\n\nconsole.log('addresses | input size | parse time');\nfor (const n of [25000, 50000, 100000, 200000]) {\n  const payload = 'a@b.com,'.repeat(n);        // n valid, comma-separated recipients\n  const t0 = process.hrtime.bigint();\n  addressparser(payload);                       // blocks synchronously\n  const ms = Number(process.hrtime.bigint() - t0) / 1e6;\n  console.log(String(n).padStart(9) + ' | ' + (payload.length / 1048576).toFixed(2) + ' MB   | ' + ms.toFixed(0).padStart(7) + ' ms');\n}\n```\n\nRun:\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-dos.js\n```\n\nActual output (nodemailer 9.0.6):\n```\naddresses | input size | parse time\n    25000 | 0.19 MB   |     381 ms\n    50000 | 0.38 MB   |    1435 ms\n   100000 | 0.76 MB   |    7949 ms\n   200000 | 1.53 MB   |   25154 ms\n```\n\nEquivalent trigger through the normal send API (freezes the event loop):\n```js\nconst nodemailer = require('nodemailer');\nnodemailer.createTransport({ jsonTransport: true })\n  .sendMail({ from: 'a@b.com', to: 'a@b.com,'.repeat(150000), subject: 'x', text: 'y' });\n// ~15+ seconds of 100% CPU inside addressparser before anything is sent\n```\n\n### Impact\n\n* **Who is impacted:** any service that runs Nodemailer (or the standalone `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, a `Reply‑To`/`From` derived from user input, a contact‑import or mailing‑list parser, or any endpoint that validates addresses with `addressparser`. No authentication, special option, or particular receiver is needed.\n\n## Patched in 9.1.0\n\nThree separate quadratic paths were fixed, not one:\n\n* `addressparser` rebuilt its accumulator with `concat()` on every address ([9116da9](https://github.com/nodemailer/nodemailer/commit/9116da9)).\n* The display-name merge loop directly below spliced each fragment out of the array, the same shape reached through `'a, b \u003cc@d.com\u003e,'.repeat(n)` (same commit).\n* `MimeNode#_convertAddresses` checked recipient uniqueness with a linear scan per address ([7cc38af](https://github.com/nodemailer/nodemailer/commit/7cc38af), refined in [34da642](https://github.com/nodemailer/nodemailer/commit/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 after `addressparser` was fixed.\n\nFixed alongside: `[].concat.apply` in `_parseAddresses` threw `RangeError: Maximum call stack size exceeded` past roughly 124k recipients, with no crafted input needed ([83b8c48](https://github.com/nodemailer/nodemailer/commit/83b8c48)).\n\nParsing 200k addresses now takes ~80ms instead of ~25s, and every path scales linearly. A new `maxRecipients` option (default 100000) throws rather than truncating, as a backstop.","status":"published","published":"2026-09-08T21:33:17Z","modified":"2026-09-17T11:56:03.050481347Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400","CWE-407"],"updated_at":"2026-11-02T15:46:35.050232Z"},{"id":"GHSA-cc9r-2j5m-2m83","title":"Nodemailer: Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain","description":"### Summary\n\nNodemailer'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 as `user@good-corp.com(x)evil.com` is parsed and **delivered to `good-corp.comevil.com`** (registrable domain `comevil.com`, attacker‑controlled), while a conformant RFC 5322 parser terminates the domain at the comment and reads `good-corp.com`.\n\nAn 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).\n\nSeverity 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`).\n\n### Details\n\nRoot cause is in `lib/addressparser/index.js`.\n\n1. The tokenizer registers the comment as an operator pair (`Tokenizer.operators`):\n   ```js\n   '(': ')',            // line ~331\n   ```\n2. When the **closing** `)` is immediately followed by a non‑break character (anything other than space / tab / CR / LF / `,` / `;`), the tokenizer marks that operator token with `noBreak = true`:\n   ```js\n   // Tokenizer.checkChar, lines ~398-399\n   if (nextChr \u0026\u0026 ![' ', '\\t', '\\r', '\\n', ',', ';'].includes(nextChr)) {\n       this.node.noBreak = true;\n   }\n   ```\n3. `_handleAddress` then **glues** the token that follows the comment onto the token that preceded it (dropping the comment):\n   ```js\n   // _handleAddress, lines ~187-188\n   if (prevToken \u0026\u0026 prevToken.noBreak \u0026\u0026 data[state].length) {\n       data[state][data[state].length - 1] += token.value;   // \u003c-- concatenation\n   }\n   ```\n\nFor the input `user@good-corp.com(x)evil.com` the tokens are `text:\"user@good-corp.com\"`, `op:\"(\"`, `text:\"x\"`, `op:\")\"` (flagged `noBreak`), `text:\"evil.com\"`. Step 3 appends `evil.com` onto `user@good-corp.com`, producing the single domain **`good-corp.comevil.com`**. The comment content (`x`) is discarded into the display‑name field.\n\nRFC 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 of `good-corp.com(x)evil.com` is the domain `good-corp.com` (with the trailing `evil.com` being invalid/ignored). Nodemailer instead concatenates the two atoms across the removed comment, yielding a different, attacker‑registrable domain.\n\nNodemailer uses the parsed address for **both** the SMTP envelope (`getEnvelope()` → `RCPT TO`) and the emitted `To:`/`From:` headers, so the entire message is routed to the concatenated domain.\n\n**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 as `user@x.com(a(b)c)` is mis‑balanced and mangled to `x.comc)`. That particular output contains a stray `)` and is **rejected** by a conformant MTA (501) — a bounce/robustness issue, not a misroute.\n\n**Suggested fix:** treat a comment as folding whitespace that terminates the current token — i.e. do **not** propagate `noBreak` across a comment‑closing `)` (restrict the `noBreak` optimization 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.\n\n### PoC\n\nEnvironment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No special transport configuration is required; the discrepancy is in address parsing.\n\n`poc-comment.js`:\n```js\n'use strict';\nconst net = require('net');\nconst nodemailer = require('nodemailer'); // 9.0.6\n\nconst TRUSTED   = 'good-corp.com';\nconst RECIPIENT = 'user@good-corp.com(x)evil.com'; // RFC 5322 comment (x) between two domains\n\n// tiny SMTP sink that prints the literal RCPT TO nodemailer transmits\nconst server = net.createServer(sock =\u003e {\n  let buf = ''; sock.write('220 sink\\r\\n');\n  sock.on('data', d =\u003e { buf += d; let i;\n    while ((i = buf.indexOf('\\r\\n')) \u003e= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);\n      const u = line.toUpperCase();\n      if (u.startsWith('EHLO')) sock.write('250-sink\\r\\n250 8BITMIME\\r\\n');\n      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits :', line); sock.write('250 ok\\r\\n'); }\n      else if (u.startsWith('DATA')) sock.write('354 go\\r\\n');\n      else if (line === '.') sock.write('250 ok\\r\\n');\n      else if (u.startsWith('QUIT')) { sock.write('221 bye\\r\\n'); sock.end(); }\n      else sock.write('250 ok\\r\\n'); } });\n});\nserver.listen(0, '127.0.0.1', async () =\u003e {\n  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });\n  await t.sendMail({ from: 'app@good-corp.com', to: RECIPIENT, subject: 'hi', text: 'x' });\n  t.close(); server.close();\n});\n```\n\nRun:\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-comment.js\n```\n\nActual output (nodemailer 9.0.6):\n```\nnodemailer transmits : RCPT TO:\u003cuser@good-corp.comevil.com\u003e\n```\n\nThe application asked to mail `user@good-corp.com(x)evil.com`; Nodemailer delivers to `good-corp.comevil.com` — registrable domain `comevil.com`, which an attacker can register.\n\n**Verified against a real RFC 5321 server** (containerized lab included with this report — `docker compose up --build`, case `R8_comment_glue`, receiver = `aiosmtpd`):\n```\nwire RCPT TO                     : RCPT TO:\u003cuser@good-corp.comevil.com\u003e\nreal server                      : ACCEPTED (250)\nrecipient parsed by real server  : user@good-corp.comevil.com   (domain good-corp.comevil.com)\ndelivered To header              : x \u003cuser@good-corp.comevil.com\u003e\n```\n\n**Which parser sees what** (the crux of exploitability):\n\n| Parser used by the application to gate/route | Domain it reads from `user@good-corp.com(x)evil.com` | Deceived? |\n|---|---|---|\n| Python `email.policy.default` (strict RFC 5322) | `good-corp.com` *(flags `InvalidHeaderDefect`)* | Yes, if defects are not checked |\n| Naive prefix / substring allow‑list (`startsWith`/`includes('@good-corp.com')`) | `good-corp.com` | Yes |\n| Nodemailer's own `addressparser` | `good-corp.comevil.com` | No |\n| Python `email.utils.getaddresses` | `good-corp.comevil.com` | No |\n| WHATWG `url.domainToASCII` | `good-corp.com(x)evil.com` | No |\n\n### Impact\n\n\n\n* **Who is impacted:** applications that make a security or routing decision on the recipient **domain** using a parser that terminates the domain at the comment, while relying on Nodemailer for delivery — specifically those that validate with a strict RFC 5322 parser **without inspecting parse defects**, or with a **prefix/substring/allow‑list** check (e.g. \"only send to `@good-corp.com`\", employee‑only flows, \"same‑tenant\" routing). Applications that validate with Nodemailer's own `addressparser`, `email.utils.getaddresses`, or `url.domainToASCII` are **not** affected, which is why this is rated below the IDN/Punycode issue.\n\n## Patched in 9.1.0\n\nFixed in [902b63e](https://github.com/nodemailer/nodemailer/commit/902b63e).\n\nNot propagating `noBreak` across the closing `)` on its own breaks valid addresses, because CFWS is legal on either side of the `@`: `user@(x)good-corp.com` and `user(x)@good-corp.com` both come out mangled. A comment now joins what it separates only when one side carries the `@`, so those keep resolving while `user@good-corp.com(x)evil.com` terminates at `good-corp.com`.\n\nQuoted-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 yields `user@x.com`.","status":"published","published":"2026-09-08T21:32:51Z","modified":"2026-09-17T11:56:01.670395333Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-20","CWE-436"],"updated_at":"2026-11-02T15:46:44.927642Z"},{"id":"GHSA-wmmp-3585-3rmp","title":"Nodemailer: IDN/Punycode domain allow-list bypass leads to email delivery to an attacker-controlled domain","description":"### Summary\n\nNodemailer 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's `url.domainToASCII`, Python's `idna`). Its address normalizer (`_normalizeAddress` in `lib/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**.\n\nAn 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.\n\nBecause 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.\n\n### Details\n\n`lib/mime-node/index.js` → `_normalizeAddress(address)` (around lines 1307–1346) splits the address at the last `@` and normalizes the domain like this:\n\n```js\n// lib/mime-node/index.js\ntry {\n    if (/[\\x80-￿]/.test(user)) {\n        encodedDomain = punycode.toUnicode(domain.toLowerCase());   // line ~1338\n    } else {\n        encodedDomain = punycode.toASCII(domain.toLowerCase());     // line ~1340\n    }\n} catch (_err) {\n    // keep domain as supplied\n}\nreturn `${this._normalizeLocalPart(user)}@${encodedDomain}`;         // line ~1346\n```\n\n`punycode` here 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:\n\n* removing **Ignored** code points such as `U+00AD` SOFT HYPHEN,\n* **Mapping** full‑width / compatibility characters to their canonical ASCII forms,\n* Unicode **NFC** normalization,\n* validity checks.\n\nAs a result, for any domain containing a UTS‑46‑mapped or ‑ignored character, Nodemailer’s `punycode.toASCII(...)` produces a **different A‑label** than `url.domainToASCII(...)` (Node ≥ 7 / WHATWG), `new URL('http://'+domain)`, browsers, and Python’s `idna` (`uts46=True`). Nodemailer then uses its A‑label as:\n\n* the SMTP envelope recipient written to the wire as `RCPT TO:\u003clocal@xn--…\u003e` (`getEnvelope()` → `lib/smtp-connection/index.js` `_setEnvelope`), **and**\n* the address emitted in the `To:` / `From:` headers (`_convertAddresses`).\n\nSo 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):\n\n| recipient (raw) | UTS‑46 parser (`url.domainToASCII`) | Nodemailer delivers to |\n|---|---|---|\n| `victim@compa{U+00AD}ny.com` (invisible soft hyphen) | `company.com` | `xn--company-pka.com` |\n| `victim@ｃｏｍｐａｎｙ.com` (full‑width) | `company.com` | `xn--mi7cd4afch9d.com` |\n| `user@exámple.com` (NFD `a`+U+0301) | `xn--exmple-qta.com` | `xn--example-vge.com` |\n\nThis 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_convert` library). 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.\n\n**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 raw `punycode.toASCII`, and decode with the matching UTS‑46 `domainToUnicode`. At minimum, reject a domain whose value changes under UTS‑46 mapping (i.e. `punycode.toASCII(d)` ≠ `url.domainToASCII(d)`).\n\n### PoC\n\nEnvironment: Node.js ≥ 18, the published `nodemailer@9.0.6`. No special configuration; the discrepancy is in domain normalization itself.\n\n`poc-idn.js`:\n\n```js\n'use strict';\nconst net = require('net');\nconst url = require('url');\nconst nodemailer = require('nodemailer'); // 9.0.6\n\nconst TRUSTED   = 'company.com';                        // the only domain the app will mail\nconst RECIPIENT = 'victim@compa\\u00ADny.com';           // attacker input: invisible U+00AD inside \"company\"\n\n// The app's domain allow-list check, done the standard (UTS-46 / browser / WHATWG) way:\nconst seen = url.domainToASCII(RECIPIENT.split('@').pop());\nconsole.log('validator (url.domainToASCII) sees:', JSON.stringify(seen),\n            seen === TRUSTED ? '=\u003e ALLOWED (equals trusted domain)' : '');\n\n// A tiny SMTP sink that prints the literal RCPT TO Nodemailer transmits:\nconst server = net.createServer(sock =\u003e {\n  let buf = ''; sock.write('220 sink\\r\\n');\n  sock.on('data', d =\u003e { buf += d; let i;\n    while ((i = buf.indexOf('\\r\\n')) \u003e= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);\n      const u = line.toUpperCase();\n      if (u.startsWith('EHLO')) sock.write('250-sink\\r\\n250 8BITMIME\\r\\n');\n      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits             :', line); sock.write('250 ok\\r\\n'); }\n      else if (u.startsWith('DATA')) sock.write('354 go\\r\\n');\n      else if (line === '.') sock.write('250 ok\\r\\n');\n      else if (u.startsWith('QUIT')) { sock.write('221 bye\\r\\n'); sock.end(); }\n      else sock.write('250 ok\\r\\n'); } });\n});\nserver.listen(0, '127.0.0.1', async () =\u003e {\n  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });\n  await t.sendMail({ from: 'app@company.com', to: RECIPIENT, subject: 'reset your password', text: 'secret link' });\n  t.close(); server.close();\n});\n```\n\nRun:\n\n```\nnpm init -y \u0026\u0026 npm install nodemailer@9.0.6\nnode poc-idn.js\n```\n\nActual output (Nodemailer 9.0.6):\n\n```\nvalidator (url.domainToASCII) sees: \"company.com\" =\u003e ALLOWED (equals trusted domain)\nnodemailer transmits             : RCPT TO:\u003cvictim@xn--company-pka.com\u003e\n```\n\nThe application’s domain check approves `company.com`, but the message is sent to `xn--company-pka.com` — a **different domain an attacker can register** — carrying the `To:` header `\u003cvictim@xn--company-pka.com\u003e` as well.\n\nA containerized version that proves the same result against a **real RFC 5321 SMTP server** (`aiosmtpd`) is included alongside this report (`docker compose up --build`, cases `R6`/IDN); the receiving server accepts `RCPT TO:\u003cvictim@xn--company-pka.com\u003e` and reports the recipient domain as `xn--company-pka.com`.\n\n### Impact\n\nAny 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:\n  * recipient **allow‑list / block‑list / “same corporate domain” checks** implemented with `new URL()`, `url.domainToASCII`, a browser‑side check, or an IDN library;\n  * flows that **display or log the normalized recipient domain** for human confirmation (the shown `company.com` differs from the delivered `xn--company-pka.com`);\n  * any domain‑gated feature (employee‑only registration, “send only to our tenant”, notification routing).\n\n## Patched in 9.1.0\n\nDomain encoding now applies UTS-46 ([259c32d](https://github.com/nodemailer/nodemailer/commit/259c32d)), so `victim@compa­ny.com` resolves to `company.com`, matching `url.domainToASCII` and browsers.\n\nOne caveat on the suggested remediation, hardened in [b212ac4](https://github.com/nodemailer/nodemailer/commit/b212ac4): `url.domainToASCII` is 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, since `user@attacker.example/mail.corp.example` encodes to the deliverable `user@attacker.example` where the bundled Punycode codec left it intact and unroutable. Those characters are now kept away from the mapper.\n\nOn severity, \"attacker-registrable\" is doing significant work in the report: `xn--company-pka.com` decodes to a label containing U+00AD and `xn--mi7cd4afch9d.com` to 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.","status":"published","published":"2026-09-08T21:33:32Z","modified":"2026-09-17T11:56:00.274908066Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-20","CWE-436"],"updated_at":"2026-11-02T15:46:58.767335Z"},{"id":"UBUNTU-CVE-2026-92596","description":"Nodemailer before 9.1.0 contains a quadratic time complexity vulnerability in the addressparser component that allows remote attackers to cause denial of service by supplying a crafted comma-separated address list. Attackers can send a single email with a large number of addresses to block the Node.js event loop for extended periods, consuming 100% CPU and freezing the process.","status":"published","published":"2026-09-16T22:18:00Z","modified":"2026-10-28T20:18:17.79177647Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"},{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"updated_at":"2026-11-02T16:57:40.980117Z"},{"id":"CLSA-2026-1784596338","title":"TuxCare security update for nodemailer (1 CVE)","description":"TuxCare rebuilt nodemailer to address 1 CVE. Fixed in 2.7.2-tuxcare.3.","status":"published","published":"2026-08-04T14:52:45Z","modified":"2026-10-15T09:47:36.400931701Z","updated_at":"2026-11-02T15:43:46.482572Z"},{"id":"CLSA-2026-1789040951","title":"TuxCare security update for nodemailer (4 CVEs)","description":"TuxCare rebuilt nodemailer to address 4 CVEs. Fixed in 6.10.1-tuxcare.8.","status":"published","published":"2026-09-10T11:49:11Z","modified":"2026-10-15T09:47:55.724835289Z","updated_at":"2026-11-02T15:43:47.938828Z"},{"id":"CLSA-2026-1790202437","title":"TuxCare security update for nodemailer (4 CVEs)","description":"TuxCare rebuilt nodemailer to address 4 CVEs. Fixed in 2.7.2-tuxcare.6.","status":"published","published":"2026-09-23T22:27:17Z","modified":"2026-10-15T09:47:56.451035591Z","updated_at":"2026-11-02T15:43:48.28229Z"},{"id":"CLSA-2026-1791511288","title":"TuxCare security update for nodemailer (4 CVEs)","description":"TuxCare rebuilt nodemailer to address 4 CVEs. Fixed in 9.0.1-tuxcare.1.","status":"published","published":"2026-10-09T02:01:28Z","modified":"2026-10-15T09:48:04.402139932Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1791795466","title":"TuxCare security update for nodemailer (1 CVE)","description":"TuxCare rebuilt nodemailer to address 1 CVE. Fixed in 9.0.5-tuxcare.1.","status":"published","published":"2026-10-12T08:57:46Z","modified":"2026-10-15T09:46:04.066940175Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1791806342","title":"TuxCare security update for nodemailer (2 CVEs)","description":"TuxCare rebuilt nodemailer to address 2 CVEs. Fixed in 9.0.3-tuxcare.1.","status":"published","published":"2026-10-12T11:59:02Z","modified":"2026-10-15T09:47:40.149152222Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1791806459","title":"TuxCare security update for nodemailer (11 CVEs)","description":"TuxCare rebuilt nodemailer to address 11 CVEs. Fixed in 9.0.6-tuxcare.1.","status":"published","published":"2026-10-12T12:00:58Z","modified":"2026-10-15T09:47:40.200342402Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1791814880","title":"TuxCare security update for nodemailer (5 CVEs)","description":"TuxCare rebuilt nodemailer to address 5 CVEs. Fixed in 9.0.5-tuxcare.2.","status":"published","published":"2026-10-12T14:21:20Z","modified":"2026-10-15T09:47:58.565885886Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1791814986","title":"TuxCare security update for nodemailer (2 CVEs)","description":"TuxCare rebuilt nodemailer to address 2 CVEs. Fixed in 9.0.2-tuxcare.1.","status":"published","published":"2026-10-12T14:23:06Z","modified":"2026-10-15T09:47:40.235758023Z","updated_at":"2026-11-02T15:43:50.599064Z"},{"id":"CLSA-2026-1792115498","title":"TuxCare security update for nodemailer (3 CVEs)","description":"TuxCare rebuilt nodemailer to address 3 CVEs. Fixed in 9.0.0-tuxcare.3.","status":"published","published":"2026-10-16T01:51:38Z","modified":"2026-10-16T07:00:02.275499464Z","updated_at":"2026-11-02T15:43:50.949147Z"},{"id":"ROOT-APP-NPM-GHSA-268h-hp4c-crq3","title":"GHSA-268h-hp4c-crq3 in nodemailer - Patched by Root","description":"Root has patched GHSA-268h-hp4c-crq3 in the nodemailer package for Root:npm. Multiple fixed versions available.","status":"published","published":"2026-10-19T13:20:22Z","modified":"2026-10-19T14:15:03.212695243Z","updated_at":"2026-11-02T15:53:27.940619Z"},{"id":"ROOT-APP-NPM-GHSA-2x7j-588g-ccc2","title":"GHSA-2x7j-588g-ccc2 in nodemailer - Patched by Root","description":"Root has patched GHSA-2x7j-588g-ccc2 in the nodemailer package for Root:npm. Multiple fixed versions available.","status":"published","published":"2026-10-19T13:20:22Z","modified":"2026-10-19T14:15:11.176240052Z","updated_at":"2026-11-02T15:53:27.940619Z"}]}}
