{"id":"GHSA-g53g-w8rj-fmg7","summary":"xmldom PI grammar regex ReDoS: quadratic backtracking on unterminated processing instructions","details":"## Summary\n\n`@xmldom/xmldom`'s processing-instruction (PI) grammar regex exhibits quadratic-time backtracking\n(ReDoS) when parsing an **unterminated** processing instruction. A single small XML document\ncontaining `\u003c?` + a target + a long run of whitespace and no closing `?\u003e` forces the regular\nexpression engine into O(n²) work, stalling the Node.js event loop. The input is parsed with\n`DOMParser.parseFromString` under **default options**, so it is reachable from unauthenticated,\nnetwork-delivered XML (SOAP/SAML, webhooks, uploads, XML APIs).\n\n## Details\n\nThe PI production in `lib/grammar.js` compiles (flags `mu`) to:\n\n```\n^\u003c\\?(NameChars)(?:[\\x20\\x09\\x0D\\x0A]+([Char]*?))?\\?\u003e\n                     ^^^ S+ greedy       ^^^ Char*? lazy\n```\n\n- `lib/grammar.js` line 261: https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/grammar.js#L261\n\nIn the optional tail `(?:S+(Char*?))?`, both the greedy separator `S+` and the lazy data `Char*?`\nmatch XML whitespace. When the required trailing `?\u003e` is absent, the engine must ultimately fail —\nbut first it tries every partition of the whitespace run between `S+` and `Char*?`, which is O(n²)\nin the length of the trailing whitespace.\n\nThe regex is executed against the **entire remaining source string** in two places in `lib/sax.js`,\nso the whole whitespace tail is scanned:\n\n- `parsePI` — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/sax.js#L680-L691\n- `parseProcessingInstruction` — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/sax.js#L862-L879\n\n## Affected Versions\n\nOnly the `0.9.x` line is affected. `lib/grammar.js` (and this PI regex) was introduced in\ncommit `726b471` (\"fix!: preserve DOCTYPE internal subset (#498)\"), first released in\n**0.9.0-beta.9**, and is unchanged through **0.9.10**.\n\nThe `0.8.x` line (≤ 0.8.13) and the unscoped `xmldom` package (≤ 0.6.0) parse PIs via a different\ncode path bounded by `indexOf('?\u003e')` — they do **not** contain this regex and are **not affected**\nby this issue. (They were not separately tested for a *different* PI ReDoS; the scope here is the\nspecific `grammar.js` regex.)\n\n| Line | PI code path | Affected? |\n|---|---|---|\n| `0.9.x` (0.9.0-beta.9 … 0.9.10) | `grammar.js` `PI` regex over full remaining source | **Yes** |\n| `0.8.x` (≤ 0.8.13) | `parseInstruction`, bounded by `indexOf('?\u003e')` | No |\n| unscoped `xmldom` (≤ 0.6.0) | older `indexOf('?\u003e')`-bounded parsing | No |\n\n## Proof of Concept\n\n```js\nconst { DOMParser } = require('@xmldom/xmldom');\nconst n = 32 * 1024;\nconst payload = '\u003ca\u003e\u003c?p' + ' '.repeat(n); // unterminated PI, no `?\u003e`\nconsole.time('parse');\nnew DOMParser().parseFromString(payload, 'text/xml');\nconsole.timeEnd('parse');\n```\n\nMeasured (Node 18), trailing whitespace after `\u003c?p`, no `?\u003e` — time quadruples per doubling of\ninput length (canonical O(n²)):\n\n| Trailing whitespace | `g.PI.exec` | `parseFromString` |\n|---|---|---|\n| 2 KB  | 4.4 ms    | 5.1 ms   |\n| 4 KB  | 16.9 ms   | 17.0 ms  |\n| 8 KB  | 111.4 ms  | 66.3 ms  |\n| 16 KB | 263.8 ms  | 336.5 ms |\n| 32 KB | 1073.1 ms | —        |\n\n## Impact\n\nAvailability only: a single parse of a small crafted document blocks the Node.js event loop for the\nduration of the quadratic scan (≈1 s at 32 KB; multi-second with larger inputs). No memory blow-up,\nno data exposure, no integrity impact. Because XML is routinely accepted from untrusted sources and\nparsed with default options, one request can stall a server.\n\n## Fix Applied\n\nFixed in `@xmldom/xmldom` **0.9.11** (`0.9.x`-only; the `0.8.x` LTS line and the\nunscoped `xmldom` package use a different, bounded PI code path and are not affected).\n\nPR [#1039](https://github.com/xmldom/xmldom/pull/1039) inserts a fixed-width negative lookahead\n`(?!\\s)` immediately after the greedy `S+`, so the separator can no longer hand whitespace back to\nthe lazy data group:\n\n```\n- var PI = reg(/^\u003c\\?/, '(', Name, ')', regg(S, '(', Char, '*?)'), '?', /\\?\u003e/);\n+ var PI = reg(/^\u003c\\?/, '(', Name, ')', regg(S, '(?!', _SChar, ')(', Char, '*?)'), '?', /\\?\u003e/);\n```\n\nThe change is correct, minimal, and behavior-preserving: it produces identical `[target, data]`\ncaptures on all valid PIs tested (incl. whitespace-heavy, tab/newline, empty-data, and xml-decl\ncases) and removes the backtracking blow-up (linear, ~0.4 ms at 128 KB after the fix). The lookahead\nis fixed-width and cannot itself backtrack — a strict improvement with no new parsing risk.\n\n## Severity note\n\nThe complexity is **quadratic**, not exponential, so a multi-second stall requires\ntens-to-hundreds of KB of input. `VA:H` reflects that xmldom applies **no input-size limit** and the\npath runs on default-options parsing, so a single unbounded parse can fully stall the event loop.","aliases":["CVE-2026-83606"],"modified":"2026-09-08T20:45:04.316950373Z","published":"2026-09-08T20:31:50Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-09-08T20:31:50Z","nvd_published_at":"2026-09-01T15:17:38Z","cwe_ids":["CWE-1333","CWE-400"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/xmldom/xmldom/security/advisories/GHSA-g53g-w8rj-fmg7"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-83606"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/pull/1039"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/commit/73df6b8bdbd86f904b9e8c3ab9c49aa54ef2802e"},{"type":"PACKAGE","url":"https://github.com/xmldom/xmldom"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/releases/tag/0.9.11"}],"affected":[{"package":{"name":"@xmldom/xmldom","ecosystem":"npm","purl":"pkg:npm/%40xmldom/xmldom"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.9.0-beta.9"},{"fixed":"0.9.11"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 0.9.10","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-g53g-w8rj-fmg7/GHSA-g53g-w8rj-fmg7.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"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"}]}