Nodemailer shipped 9.1.0 on 2026-08-31 and two advisories landed the next day. Both say a version of the same thing: the recipient domain your code checked is not the recipient domain nodemailer hands to the mail server.
We had a reason to care beyond the advisory. A week earlier we published a post about an Auth.js magic-link flaw and said plainly that nobody had tested whether nodemailer completes that attack chain. So we installed both versions and measured it.
TL;DR
Both advisories are real and nodemailer 9.1.0 fixes both, confirmed against the actual SMTP envelope rather than the parser alone. Two things worth knowing that the advisories do not say. The crude validator was the safe one: an endsWith("@company.com") check blocked the comment payload, while a standards-compliant one waved it through. And nodemailer does not apply NFKC normalization, so the Auth.js homoglyph attack does not complete through it.
What the two advisories say
Disclosed 2026-09-01, both CVSS 6.5 Moderate, both fixed in 9.1.0, neither carrying a CVE. Note that the GitHub Advisory Database only ingested them on 2026-09-08, so any tool reporting that later date is showing you the filing date rather than the disclosure.
GHSA-cc9r-2j5m-2m83, affecting >=6.9.16 <9.1.0. RFC 5322 permits comments in parentheses inside an address. When a closing parenthesis is immediately followed by a non-break character, nodemailer's parser sets noBreak = true and concatenates what follows onto the preceding token. So user@good-corp.com(x)evil.com becomes user@good-corp.comevil.com.
GHSA-wmmp-3585-3rmp, affecting <9.1.0. Nodemailer bundled a raw RFC 3492 punycode codec with no UTS-46 mapping, while browsers and Node's own url.domainToASCII do apply UTS-46. Put a soft hyphen (U+00AD) inside a domain and the two disagree about what domain it even is.
What we measured
Rather than trust the summaries, we installed nodemailer@9.0.0 and nodemailer@9.1.0 and sent through jsonTransport, which builds the real message and envelope without delivering it. The envelope is what matters. It's the address handed to the SMTP server, and it's the one the advisories are about.
const nodemailer = require('nodemailer');
const t = nodemailer.createTransport({ jsonTransport: true });
// U+00AD SOFT HYPHEN sits between "compa" and "ny"
const payloads = [
'staff@company.com(x)evil.com',
'staff@company.com',
];
for (const to of payloads) {
const info = await t.sendMail({ from: 'app@example.com', to, subject: 'x', text: 'y' });
console.log(to, '->', info.envelope.to);
}
Results, run 2026-09-09:
| Payload | 9.0.0 envelope | 9.1.0 envelope |
|---|---|---|
staff@company.com(x)evil.com | staff@company.comevil.com | staff@company.com |
staff@compa<U+00AD>ny.com | staff@xn--company-pka.com | staff@company.com |
Both bugs reproduce exactly as described, and both are closed in 9.1.0. company.comevil.com and xn--company-pka.com are ordinary domains that anyone can register.
The password reset is the version that hurts. An allowlist bypass on signup gets a stranger an account. The same primitive pointed at a password reset gets them somebody else's account, because the reset token is in the mail and the mail went to a domain they own. If your app restricts anything by email domain, this is the flow to check first.
The finding the advisories do not mention
We also ran each payload past the three domain checks an app like this actually contains, to see which ones the attack gets past.
| Validator | company.com(x)evil.com | compa<U+00AD>ny.com |
|---|---|---|
email.endsWith("@company.com") | blocked | blocked |
email.split("@")[1] === "company.com" | blocked | blocked |
domainToASCII(email.split("@")[1]) === "company.com" | blocked | allowed |
Read the first column. The comment payload is stopped by every check we tried, including the two crude ones, because the raw string genuinely does not end in @company.com. To be exploited, that bug needs a validator that parses RFC 5322 comments correctly and therefore agrees the domain is company.com, while nodemailer disagrees. That means a proper address-parsing library, not a string comparison.
Now the second column. The soft-hyphen payload walks past exactly one validator, and it's the good one. domainToASCII applies UTS-46, sees company.com, and approves. Nodemailer 9.0.0 then delivers to xn--company-pka.com.
Both bypasses need your validator to be more standards-compliant than your sender. A founder who wrote email.endsWith("@company.com") because they didn't know any better was accidentally safe from both. A founder who normalized properly with new URL() or domainToASCII was the one who could be hit. That is not an argument for writing worse code. It's the reason the fix isn't "validate harder".
About the Auth.js magic-link chain
Our post on the Auth.js magic-link homoglyph flaw said the attack needs the downstream mail sender to apply Unicode NFKC normalization, that the advisory named no sender that does, and that nobody had published results for nodemailer. We can close the nodemailer half now.
The payload is an address with a fullwidth commercial at, U+FF20, which NFKC folds into a real @. Send victim@company.com@evil.com and a normalizing sender would turn it into victim@company.com plus a trailing @evil.com, changing which domain receives the mail.
nodemailer 9.0.0 envelope.to = ["victim@company.com@evil.com"]
nodemailer 9.1.0 envelope.to = ["victim@company.com@evil.com"]
The fullwidth character survives untouched in both versions. The domain stays evil.com, which is where the mail was always going, so nothing is redirected and nothing is gained. Nodemailer does not NFKC-normalize recipient addresses, so the Auth.js homoglyph attack does not complete through it.
That's a negative result and it's worth publishing precisely because it's negative. If you run Auth.js with the nodemailer provider, that particular chain is not your problem. Resend, SendGrid and Postmark remain untested, by us and by anyone else we can find.
What to do
Check your version: npm ls nodemailer. Anything below 9.1.0 has both bugs. Nodemailer 10.0.1 is current, so upgrade there unless something pins you back.
Search your code for a decision made on the recipient domain. Invite-only signup gated to one company, a "corporate accounts only" branch, a support inbox routing rule. No domain decision means nothing to bypass, and you can stop here.
Where you find one, make it normalize once and send to the normalized value. Don't validate a parsed form and then hand the raw user input to your mail library, which is the shape both of these advisories punish.
A one-line check that catches the whole class. After normalizing, compare the normalized address to the raw input. If they differ, either reject it or send to the normalized version. You never want the string you inspected and the string you deliver to be two different strings.
Sources
| Claim | Source | Date |
|---|---|---|
RFC 5322 comment concatenation, >=6.9.16 <9.1.0, CVSS 6.5, no CVE | GHSA-cc9r-2j5m-2m83 | disclosed 2026-09-01 |
Raw RFC 3492 punycode with no UTS-46 mapping, <9.1.0, CVSS 6.5, no CVE | GHSA-wmmp-3585-3rmp | disclosed 2026-09-01 |
nodemailer@9.1.0 publication time | npm registry | 2026-08-31T11:29:12Z |
nodemailer@9.0.0 publication time | npm registry | 2026-06-14T17:57:22Z |
| Envelope results, validator matrix, homoglyph result | CheckYourVibe, run against 9.0.0 and 9.1.0 | 2026-09-09 |
| Auth.js homoglyph flaw and its NFKC precondition | GHSA-7rqj-j65f-68wh | disclosed 2026-07-23 |
Both advisories were ingested into the GitHub Advisory Database on 2026-09-08, a week after disclosure. We use the disclosure date throughout.
What are GHSA-cc9r-2j5m-2m83 and GHSA-wmmp-3585-3rmp?
Two nodemailer advisories disclosed on 2026-09-01, both CVSS 6.5 Moderate, both fixed in 9.1.0, and neither has a CVE assigned. The first is an RFC 5322 comment parsing bug affecting 6.9.16 up to 9.1.0. The second is an internationalized-domain bug affecting everything below 9.1.0, where nodemailer used a raw RFC 3492 punycode codec with no UTS-46 mapping. Both end in the same place: the domain your code approved isn't the domain the mail server was handed.
Am I actually affected if I run nodemailer below 9.1.0?
Only if your app decides something based on the recipient domain, such as an invite-only signup restricted to one company, and then passes the same raw string to nodemailer. If you send mail to whatever address a user typed with no domain check, there's nothing to bypass. Upgrade anyway, since it's one version bump.
Does upgrading to 9.1.0 fix both problems?
Yes. We measured the SMTP envelope directly on 9.0.0 and 9.1.0. On 9.0.0 the comment payload was delivered to company.comevil.com and the soft-hyphen payload to xn--company-pka.com. On 9.1.0 both resolved to company.com, matching what a browser and Node's own url.domainToASCII see. Nodemailer 10 is current, so most projects should go there instead.
Does this mean the Auth.js magic-link homoglyph bug is exploitable through nodemailer?
No, and this is the useful negative result. That attack needs the downstream sender to apply NFKC normalization so a fullwidth commercial at (U+FF20) folds into a real @. We passed that exact payload through nodemailer 9.0.0 and 9.1.0 and the fullwidth character survived untouched in both, so the domain stayed evil.com and the chain doesn't complete. Nodemailer does not NFKC-normalize recipient addresses.
Which domain check should I use for an invite-only signup?
Compare against a normalized form and then send to that same normalized string, never to the raw input. The bug class here isn't any one parser being wrong, it's your app using two different parsers: one to decide and one to deliver. Normalize once, make the decision on the result, and pass the result downstream.
Does Your Signup Trust the Domain?
CheckYourVibe probes your deployed app's signup and reset flows with addresses that look like one domain and resolve to another.