Repository navigation
chore(deps): update dependency @xmldom/xmldom to v0.9.12 [security] - #11318
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/npm-xmldom-xmldom-vulnerability
branch
8 times, most recently
from
October 10, 2026 03:13
5985faa to
92ed9c6
Compare
renovate
Bot
force-pushed
the
renovate/npm-xmldom-xmldom-vulnerability
branch
15 times, most recently
from
October 16, 2026 23:40
81b9547 to
9ee2b96
Compare
renovate
Bot
force-pushed
the
renovate/npm-xmldom-xmldom-vulnerability
branch
4 times, most recently
from
October 21, 2026 21:33
f0b75a4 to
7403bdf
Compare
renovate
Bot
force-pushed
the
renovate/npm-xmldom-xmldom-vulnerability
branch
from
October 21, 2026 23:22
7403bdf to
e3096d6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
0.9.11→0.9.12Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
xmldom: DocType
nameInjection Bypasses requireWellFormedCVE-2026-83608 / GHSA-27p8-2357-5qqv
More information
Details
Summary
The
@xmldom/xmldomserializer emitsDocumentType.nameverbatim into the<!DOCTYPE …>declaration with no well-formedness guard. GHSA-f6ww-3ggp-fr8h(CVE-2026-41674) hardened the serializer's
requireWellFormedpath for aDocumentType's sibling fields —
publicId,systemId, andinternalSubset—but it did not add any check for
name. A>(or whitespace) in the nameterminates the doctype declaration early, letting the remaining characters
become sibling markup in the serialized output.
Because
requireWellFormed: true— the recommended mitigation for the priorxmldom injection CVEs — performs no validation on the DocType
name, this is abypass of that control, in the same family as the open element-name
(GHSA-w2rr-34g9-rvrj) and attribute-name (GHSA-4w3w-2rp5-g8jm) name-injection advisories.
Details
The serializer's
DOCUMENT_TYPE_NODEcase runs therequireWellFormedblockonly against
publicId,systemId, andinternalSubset, then pushesn.namedirectly into the buffer between the<!DOCTYPEprefix and theclosing
>:bb7a085):serializer DocType case,
lib/dom.js#L3256-L3283— the
requireWellFormedblock (#L3259-L3269)validates
publicId/systemId/internalSubsetbut notname, which isemitted verbatim at #L3270.
e5c1480):serializer DocType case,
lib/dom.js#L1914-L1946— same structure;
nameis emitted verbatim at #L1928.xmldom(v0.6.0,c80a161):lib/dom.js#L1105emits
node.nameverbatim; this line predatesrequireWellFormed, so thereis no well-formedness path at all.
Enabling write paths
DocumentType.nameis a plain, writable own-property, so the enabling vectordiffers by line:
createDocumentType()validates the name viavalidateQualifiedName(lib/dom.js#L925-L936,validation at #L926),
so the deliverable vector is a direct property write
(
dt.name = 'html><script>…') to the unguarded own-property.createDocumentType()does not validate the name(
lib/dom.js#L456-L464),so the malicious name is reachable directly through
createDocumentType()aswell as via direct property write.
xmldom(<= 0.6.0) —createDocumentType()does not validatethe name (
lib/dom.js#L286),same as 0.8.x.
This is the same structural root cause the sibling name-injection advisories
share: the serializer's
requireWellFormedpath validates content delimitersbut no name field, and every name-like field is a plain writable property, so
mutation / direct property-write bypasses any creation-time check.
Root Cause
requireWellFormedDocType block checkspublicId,systemId, andinternalSubset(the fields hardened by GHSA-f6ww-3ggp-fr8h)but has no check for
name.DocumentType.nameis a plain writable own-property; on 0.8.x and theunscoped package
createDocumentType()does not validate it either.namedirectly between the doctype delimiters:<!DOCTYPE ${name}…>.Proof of Concept
Run against
@xmldom/xmldomv0.9.10 (commitbb7a085):Confirmed runtime behavior:
createDocumentType()rejects the malicious name at creation(
InvalidCharacterError); a direct write todt.namebypasses that, andserializeToString(…, { requireWellFormed: true })emits the breakout with noexception.
<script>is a real second top-level element originating entirely from theDocType name: the parser rejects it with
HierarchyRequestError: Only one element can be added and only after doctype.A comment-injection variant (
dt.name = 'html><!--INJECTED--', output<!DOCTYPE html><!--INJECTED--><r/>) re-parses cleanly and the injectedcomment node is enumerable, confirming the injected node is structurally live.
e5c1480) —createDocumentType('html><script>…', '', '')accepts the malicious name directly (no creation-time validation), and
serializeToString(doc, null, null, { requireWellFormed: true })produces<!DOCTYPE html><script>alert(1)</script><r/>with no exception.A browser reproduction does not apply: browsers keep
DocumentType.namereadonly, so the direct-write vector cannot be reproduced in a browser DOM.The injection is specific to xmldom exposing
nameas writable and serializingit without a guard.
Impact
Applications that build a
DocumentTypenode with an attacker-influencedname— via direct property write on any affected line, or viacreateDocumentType()on 0.8.x and the unscoped package — and serialize thedocument are vulnerable to XML/markup injection:
<!DOCTYPE …>declarationto inject arbitrary sibling elements, comments, or additional markup into the
output.
by a browser-based XML parser, an injected
<script>element (in the XHTMLnamespace) executes.
requireWellFormed: trueas a mitigation for the prior injection CVEs(including the sibling DocType fields fixed by GHSA-f6ww-3ggp-fr8h) remain
vulnerable through the DocType
name.Fix Applied
Under
requireWellFormed, the serializer validates the DocTypenameas a well-formed XMLNameand throwsInvalidStateErrorwhen it is not — matching the siblingpublicId/systemId/internalSubsetchecks. Non-breaking and opt-in; ships on both maintained versions. Nocreation-time change is made: 0.9.x already validates the name at
createDocumentType, and the0.8.x/unscoped creation gap cannot be closed without a breaking change, so it is left
unfixed. See the XML
Nameproduction.Proof of Concept - fixed path
Why the default stays verbatim
W3C DOM Parsing's require-well-formed flag defaults to false, and the browser
XMLSerializeremits the name verbatim in that default mode. Unconditionallythrowing would be an unjustified breaking change against that specified default,
so the guard is opt-in behind
{ requireWellFormed: true }.Residual limitation
The default serialization path still emits the ill-formed DocType
nameverbatim; protection applies only when
requireWellFormed: trueis passed. Nocreation-time validation is added for the DocType
name: 0.9.x already validatesat
createDocumentType, and the 0.8.x/unscoped creation gap is left unfixed — itcannot be closed without a breaking change.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: Creation-time XML Name/QName validation is bypassable via an embedded line terminator, allowing injection on the default serialization path
CVE-2026-83609 / GHSA-3px3-54cx-rmw9
More information
Details
Summary
An embedded line terminator bypasses xmldom's always-on, WHATWG-mandated creation-time name
validation.
createElementNS,createAttributeNS,createDocumentType, andcreateAttributeshouldreject a malformed qualified name with
InvalidCharacterError, but a name whose first line iswell-formed slips through and enters the DOM. On serialization it is emitted verbatim, so the
characters after the line terminator inject markup into the output. The injection reaches the default
serialization path, and enabling
requireWellFormeddoes not prevent it.Details
createElementNS,createAttributeNS, andcreateDocumentTyperoute throughvalidateQualifiedName,and
createAttributeperforms the analogous check; each validates the name withg.QName_exact.test(name).QName_exact = reg('^', QName, '$')inherits themflag from xmldom'sshared regexp builder, so the matcher accepts any name whose first line is a valid
QNameand leavesthe remaining lines unconstrained (see Root Cause).
Root Cause
mflag.^…$undermare line anchors, not string anchors.validateQualifiedName/createAttributevalidate with.test()against such a production, so aline terminator followed by breakout markup passes and the malformed name is stored.
The triggering line terminators are the ECMAScript
LineTerminatorset: U+000A, U+000D, U+2028, U+2029.Proof of Concept
Impact
InvalidCharacterError): amalformed name the standard requires be rejected at creation is instead admitted to the DOM.
create*APIs to reject malformednames (the standard behavior) as a trust boundary is exposed; where the serialized output reaches an
HTML context, downstream XSS.
path and
requireWellFormed: true— the strict serializer shares the samem-flagged blind spot(the subject of the sibling serializer advisories) — so the bypassed creation-time check was the only
layer that could have stopped it. Demonstrated for all four
create*sites inpoc_creation_strict_serialization_bypass.cjs.Fix Applied
createElementNS,createAttributeNS,createDocumentType, andcreateAttributenow reject a namecontaining a line terminator with
InvalidCharacterError— the same result they already give forother malformed names — because name validation now applies to the whole string. The
requireWellFormedserializer's name checks are corrected by the same change. The fix isnon-breaking: such a name was already invalid, and no previously-accepted well-formed name is affected.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: XML fragment injection via invalid EntityReference.nodeName during requireWellFormed serialization
CVE-2026-83610 / GHSA-6gmq-8vp8-gcm6
More information
Details
Summary
An
EntityReferencenode can be created with an invalid, attacker-controlled name throughDocument.createEntityReference(name). When this node is serialized directly with:the invalid
nodeNameis emitted into the serialized XML fragment without validation or escaping.This can produce real XML markup in the serialized output. In the proof of concept below, the serialized fragment contains
<injected/>, and reparsing the fragment creates a realinjectedelement.Details
The issue appears to be in the serialization path for
ENTITY_REFERENCE_NODE.For several other node types,
requireWellFormed: trueperforms specific validation checks before serialization. For example, comments, processing instructions, document types, and some character data cases are checked before being emitted.However, for
ENTITY_REFERENCE_NODE, the serializer appears to emit the node name directly in entity reference form:As a result, if
nodeNamecontains characters that break out of the intended&name;structure, the serializer can emit additional XML markup.For example, an entity reference created with the name:
is serialized as:
When this fragment is later parsed in an XML context,
<injected/>becomes a real element.This is especially surprising when
{ requireWellFormed: true }is used, because applications may reasonably treat this mode as the stricter or safer XML serialization mode.Proof of Concept
Tested with:
Observed output:
Impact
An application that creates an
EntityReferencefrom attacker-controlled input and then serializes that node or XML fragment withrequireWellFormed: truemay produce XML containing attacker-controlled markup.The impact is limited by two observations:
EntityReferencenodes from ordinary XML entity references.EntityReferencenode as an element child is rejected with aHierarchyRequestError.The main affected scenario is applications that directly use
createEntityReference(name)and then serialize the resulting node or fragment.Fix Applied
Two complementary, non-breaking fixes.
(1)
document.createEntityReference(name)rejects an invalidNameat creation, closing the reachable creation vector by default — the opt-in serializer check alone cannot, since a laternodeNamemutation would bypass a creation-only guard.(2) Under
requireWellFormed, the serializer validates theEntityReferencenodeNameas a well-formed XMLNameand throwsInvalidStateErrorwhen it is not; a valid reference still serializes as&name;. Both ship on both maintained versions. TheEntityReference/createEntityReferencedocs note that underrequireWellFormedthenodeNameis validated as an XMLName, and that xmldom does not expand entities. See the XMLNameproduction.Proof of Concept - fixed path
Why the default stays verbatim
The creation-time anchor is applied by default, because it is classified non-breaking. The serializer check, by contrast, stays gated behind
{ requireWellFormed: true }: W3C DOM Parsing's require-well-formed flag defaults tofalse, and the browserXMLSerializeremits thenodeNameverbatim in that default mode, so unconditionally throwing for an ill-formedEntityReference.nodeNamewould be an unjustified breaking change — which is why the default serialization path stays verbatim.Residual limitation
The creation vector is closed by default — the non-breaking creation-time anchor — with no further deferred work. The residual is at serialization: the default path still emits an ill-formed
nodeNameverbatim, because the serializer check is opt-in via{ requireWellFormed: true }.Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: Parser silently accepts a not-well-formed end tag whose name is followed by a line break and trailing content
CVE-2026-83611 / GHSA-6h8r-xr42-gp59
More information
Details
Summary
xmldom's parser silently accepts a not-well-formed end tag whose valid name is followed by
trailing content — e.g.
</a⏎junk>. The element is closed, the trailing content is discarded, and noerror is reported, even though the XML end-tag production allows only optional whitespace after the
name and both Chromium and Firefox reject such input as
application/xml. An application that relieson xmldom to reject not-well-formed input therefore receives a false "valid" result for a document the
specification and browsers consider malformed.
Details
Across every affected version, an end tag whose valid
Nameis followed by trailing content before>is silently accepted: the element is closed, the residue is dropped, and no error is reported. Howmuch leaks differs by line (see Affected Versions), but the observable weakness is the same.
On the current (
0.9.x) line, the parser validates the end-tag name against the XMLETagproductionwith an anchored regular expression (
^ QName S? $). That expression is compiled with them(multiline) flag by a shared builder, so
$matches at an interior line terminator: a valid name onthe first line satisfies the anchored production and any content after the line break escapes the
check. On 0.9.x the whitespace-separated variant (
</a junk>) is already rejected; only theline-terminator variant leaks. Older lines have no anchored end-tag validator at all, so they accept
both the line-terminator and the whitespace variant.
This is not content injection — the trailing content is dropped, and the resulting DOM is a normal
single-root document (
<a/>). The security-relevant property is the silent acceptance ofnot-well-formed input: xmldom's parse result disagrees with the specification and with browser XML
parsers, so any control that treats "xmldom parsed it without error" as "well-formed" is bypassed.
Root Cause
On the
0.9.xline, where the line-terminator variant specifically leaks:mflag.^…$undermare line anchors, not string anchors.^ QName S? $is therefore satisfied by the first line alone, sotrailing content after a line terminator is neither matched nor rejected — the malformed end tag is
accepted and the residue silently discarded.
The triggering line terminators are the ECMAScript
LineTerminatorset: U+000A, U+000D, U+2028, U+2029.U+2028 and U+2029 are not XML whitespace, so they are non-conforming trailing content that nonetheless
leaks because the JavaScript
$anchor treats them as line boundaries underm.Affected Versions
Both maintained versions are affected and fixed:
0.9.x(>= 0.9.0, <= 0.9.11) →0.9.12: the anchored end-tag validator'smflag leaksthe line-terminator variant. The whitespace variant (
</a junk>) is already rejected on this line.0.8.x(>= 0.8.0, <= 0.8.14) →0.8.15: no anchored end-tag validator at all — both theline-terminator and the whitespace variant are silently accepted; the fix adds a residue check.
release-0.7.x(<= 0.7.13) and the unscopedxmldompackage (range*, last release 0.6.0) areaffected but will not be patched — they are end-of-life / unmaintained. They share the older-line
behavior (both variants silently accepted, residue dropped).
Proof of Concept
Impact
reject is parsed without error.
reject malformed input treats such a document as valid. Note this is an input-validation /
parser-differential issue, not content injection — the trailing content is discarded.
Fix Applied
The parser now reports a not-well-formed end tag whose valid name is followed by trailing content, on both
0.9.12and0.8.15— previously it was accepted silently — and parsing recovers to the byte-identical DOM as before.On
0.9.12the anchored end-tag validator is corrected so a line break followed by trailing content no longer satisfies it, reported as a recoverableerrorwhen parsing as XML and awarningwhen parsing as HTML.On
0.8.15, which previously performed no end-tag residue validation, an equivalent residue check is added, reported as a recoverableerrorin both XML and HTML.Well-formed documents are unaffected.
Because the report is recoverable, the fix is non-breaking: no previously-parsed document begins to
throw and no serialized output changes. Consumers that want strict rejection can escalate the reported
errorto a fatal one via the parser's error handler (onErrorin0.9.12,errorHandlerin0.8.15). The two versions also differ in the whitespace-separated variant (</a junk>):0.9.12already rejected it with a fatal error in XML and continues to, while
0.8.15— which validatedneither variant — now emits the same recoverable report for both the whitespace and line-break
variants.
Residual limitation
By default the parser still recovers (it does not reject the document); the reported condition is a
recoverable
error/warning, not a fatal error, to avoid changing the parsed output in a patchrelease. Converting this and the other not-well-formed-acceptance cases to a consistent
fatalError—including the broader end-tag leniency on the older versions — is deferred to the next breaking release,
tracked at xmldom/xmldom#1074.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: HTML raw-text closing-tag case mismatch causes output amplification
CVE-2026-83612 / GHSA-6mj3-qw4j-hgrw
More information
Details
Summary
In HTML mode (
text/html), a raw-text element (script,style,textarea,title) whose closingtag differs in case from its opening tag (e.g.
</ScRiPt>for<script>) is mishandled by theparser, producing quadratic (O(n²)) output growth — a small crafted document parses and serializes
into output orders of magnitude larger, exhausting CPU and memory. A modest input of tens of KB can
therefore cause a denial of service in any service that parses untrusted HTML with xmldom. Only HTML
mode is affected.
Details
The parser calls
parseHtmlSpecialContentfor each raw-text element in HTML mode, matched viaisHTMLRawTextElement/isHTMLEscapableRawTextElement(so all four types —script,style,textarea,title— are in scope). It searches for the element's closing tag withsource.indexOf('</' + tagName + '>', elStartEnd), a byte-for-byte case-sensitive match. Amixed-case closing tag never matches, so the search returns
-1, and the followingsource.substring(elStartEnd + 1, -1)extracts text backwards from the start of the documentinstead of the element's content. The function then returns
-1to the parse loop, which cannotadvance normally and falls back to character-by-character reprocessing. Every raw-text element
re-captures all source text preceding it, so output grows as O(n²) in the number of such elements.
Root Cause
lib/sax.js:549):source.indexOf('</' + tagName + '>', elStartEnd)does not fold case, contrary to the WHATWG HTML RAWTEXT end-tag-name rule.-1(lib/sax.js:550):source.substring(elStartEnd + 1, elEndStart)runs evenwhen
elEndStart === -1, extracting text backwards from position 0.lib/sax.js:556): the function returnselEndStart(-1), drivingrepeated character-by-character fallback in the parse loop.
Affected Versions
Only the
0.9.xline is affected — the amplification was introduced in0.9.0-beta.1whenparseHtmlSpecialContentwas refactored, and remains through0.9.11. The0.8.xline is notaffected: its older
parseHtmlSpecialContentdoes not amplify, despite sharing the samecase-sensitive
indexOf.Proof of Concept
Output size grows quadratically with the number of case-mismatched raw-text elements:
Proof of Concept from @KarimTantawey (tested with
script); the same amplification occurs forstyle,textarea, andtitle.Impact
Small attacker payloads can force disproportionate CPU and memory usage in services that
parse and serialize untrusted HTML via xmldom. The quadratic growth means a modest-sized input
(tens of kilobytes) can produce output in the tens or hundreds of megabytes, potentially
exhausting memory or causing timeouts.
The attack only requires HTML mode (
text/htmlMIME type) and mixed-case closing tags forany of the four raw-text element types. No special configuration or error handler setup is needed.
Severity note
The CVSS 4.0 vector scores availability only (
VA:H, withVC:N/VI:N): the flaw neither disclosesnor corrupts data, but a small untrusted HTML input (tens of KB) can force output and memory in the
tens to hundreds of MB, enough to exhaust a service's heap or stall its event loop. It is reachable
with no authentication, configuration, or error-handler setup — only that the application parses
untrusted
text/htmland serializes the result.Fix Applied
The raw-text closing tag is now matched case-insensitively in HTML raw-text mode (per the WHATWG HTML
RAWTEXT end-tag rule),
and a missing closing tag is handled explicitly, removing the quadratic output amplification. Output
for well-formed input is unchanged. Non-breaking; 0.9.x-only.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: Quadratic-time attribute deduplication
CVE-2026-83613 / GHSA-8344-3jmq-59r6
More information
Details
Summary
xmldom builds the attribute collection of every parsed element by inserting attributes one at a
time into a DOM
NamedNodeMap. Each insertion first performs a linear scan of allalready-inserted attributes to enforce the DOM uniqueness rule (no two attributes with the same
qualified name / namespace+local-name). Parsing an element that carries
Mdistinct attributestherefore costs
1 + 2 + … + M = O(M²)comparisons.Because the trigger is simply "one element with many attributes", the attack payload is a
fully well-formed XML document. No malformed markup, no error recovery, and no non-default
parser options are involved — parsing completes silently with zero
warning/error/fatalErrorevents. An attacker who can submit a modest, highly compressible document (a single element with
tens of thousands of attributes, ~340 KB uncompressed) can consume seconds of single-threaded CPU
per request, enabling an unauthenticated denial of service.
This is distinct from the known quadratic-memory namespace-map issue: it burns CPU and it
does not require any namespace declarations or nesting.
Details
The DOM content handler adds each attribute of a starting element by calling
el.setAttributeNode(attr)in a loop:https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom-parser.js#L370-L387
setAttributeNodedelegates toNamedNodeMap.setNamedItem, which callsgetNamedItemNSto lookfor an existing attribute with the same namespace URI and local name before appending:
https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L612-L623
getNamedItemNSwalks the whole list on every call:https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L702-L715
For the i-th attribute the scan visits
i-1entries, so insertingMdistinct attributes performsΘ(M²)comparisons. There is no hash index or set keyed by name; the map is a plainarray-backed structure.
The same structure exists on 0.8.x. There
setNamedItemdedups viagetNamedItem(attr.nodeName)instead ofgetNamedItemNS, but that method is likewise a full linearscan, so the complexity is identical:
startElementloop /setAttributeNode:https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom-parser.js#L159-L176
setNamedItem→ lineargetNamedItem:https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L286-L308
The linear-scan
NamedNodeMappredates the@xmldom/xmldomfork and is present unchanged in theunscoped
xmldompackage back to its earliest published release. Inxmldom@0.1.0, parsing alreadyinserts each attribute one at a time (
DOMHandler.startElementloops callingsetAttributeNS→setAttributeNode→NamedNodeMap.setNamedItem), andsetNamedItemdedups bycalling
getNamedItemNS, which is a full linearwhile (i--)scan of the already-insertedattributes — the identical
O(M²)structure. The whole unscoped line (0.1.0…0.6.0) istherefore affected; the earliest published tag (
0.1.0) was verified to contain the per-insertlinear dedup scan.
Proof of Concept
A single well-formed element with
Mdistinct attributes. No malformed markup and no options:Measured with Node.js v18.20.8 (wall-clock; absolute numbers vary by host, the scaling is the
load-bearing fact):
@xmldom/xmldom0.9.10:@xmldom/xmldom0.8.13:xmldom(unscoped) 0.6.0: 4000 → 28.2 ms, 8000 → 131.8 ms, 16000 → 545.2 ms (≈ ×4 per doubling).Time grows ≈ ×4 per doubling of
M— quadratic. About 340 KB of well-formed input costs ~1.6–2.1 sof single-threaded CPU, and it keeps scaling: doubling the attribute count quadruples the cost.
The document is trivially generated and compresses to a few kilobytes on the wire.
Impact
Unauthenticated, remotely triggerable denial of service against any service that parses
attacker-influenced XML/HTML with xmldom. A single request holds one event-loop thread for seconds;
a handful of concurrent requests can saturate CPU and stall the process. Because the payload is a
plain well-formed document (one element, many attributes), it passes any "must be well-formed" gate
and reaches the parser before any application-level validation (e.g. schema checks or signature
verification) can run. The payload is highly compressible, so it is effective over compressed
transports.
Fix Applied
Replaced the per-insert linear duplicate scan on the parse-time dedup path with a name-keyed
index, so de-duplicating an element's attributes during parse is O(M) instead of O(M²) — a
well-formed-but-hostile attribute list can no longer wedge the parse. Behavior-preserving: attribute
order and duplicate resolution (last value wins, first position kept) are byte-identical. Non-breaking
and independent of
requireWellFormed; ships on both maintained versions.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
xmldom: Quadratic-time parsing via the malformed-input recovery path —
parseElementStartPartre-scan andnormalize()adjacent-text mergeCVE-2026-83614 / GHSA-93r5-fhx6-vmg9
More information
Details
Summary
xmldom's malformed-input error-recovery path has two quadratic-time (O(n²)) behaviors that asingle crafted input triggers together, so a tiny, highly compressible document (tens of KB) stalls
the Node.js event loop for multiple seconds. It is reachable from
DOMParser.parseFromStringunderdefault options — i.e. from unauthenticated, network-delivered XML — making this an unauthenticated
denial of service. One of the two behaviors, the
normalize()adjacent-text merge, is additionallyreachable programmatically — via a plain
normalize()call on a DOM built with adjacent text nodes,independent of the parser — so its fix must live in
normalize(), not only in a parser bound.Details
Finding A —
parseElementStartPartquadratic re-scanA
<character is not a delimiter in any tag-parsing state, soparseElementStartPartscansforward character-by-character over any embedded
<until it reaches the next>(or end ofinput), then validates the accumulated slice as a tag name and throws
invalid tagName:on failure.The main loop catches this, reports an
error, setsend = -1, and recovers by advancing a singlecharacter (
appendText(Math.max(tagStart, start) + 1)). With a long run of<and a distant>,each of the O(n) recovery retries performs an O(n) scan plus an O(n) anchored regex validation over
the growing candidate ⇒ O(n²).
Code (0.9.x,
bb7a085dc5ba1eea3212388509b97bb4b4af32b9):parseElementStartPartcharacter scan — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/sax.js#L263-L461setTagName→ throwsinvalid tagName) — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/sax.js#L886-L891catch→error+end = -1— https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/sax.js#L234-L242Code (0.8.x,
e5c14802592685bb872c042c54c3f73758875c85):parseElementStartPart— https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/sax.js#L227catch→error+end = -1— https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/sax.js#L202-L208setTagNamevalidation — https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/sax.js#L616-L621Finding B —
normalize()adjacent-text O(K²) mergeendDocument()callsdocument.normalize(). For a parent with K adjacent text nodes (produced bythe one-character recovery of Finding A),
normalize()performs K−1 merges. Each merge does aremoveChild— which re-indexes all child nodes of the parent (O(K)) — and anappendData—which rebuilds the accumulator string
this.data + text(O(K)). Total: O(K²).Well-formed XML cannot produce adjacent text-node siblings through the parser (each text run is one
node; comments, CDATA, PIs, and elements sit between runs), so the parse-path trigger for Finding B
is the malformed-input recovery that emits single-character text nodes. The same O(K²) merge is,
however, independently reachable via the public
normalize()API on a programmatically built tree(see "Finding B is additionally reachable programmatically" below).
Code (0.9.x,
bb7a085dc5ba1eea3212388509b97bb4b4af32b9):endDocument→normalize()— https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom-parser.js#L418-L420normalize()adjacent-text merge — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L1336-L1356removeChildre-index-all branch — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L1788-L1798appendDatastring rebuild — https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L2786-L2790Code (0.8.x,
e5c14802592685bb872c042c54c3f73758875c85):endDocument→normalize()— https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom-parser.js#L213-L214normalize()merge — https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L529-L549removeChildre-index-all branch — https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L756-L773appendDatastring rebuild — https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L1533Finding B is additionally reachable programmatically (no parser involved)
Node.prototype.normalize()is public API on everyDocument/Element. A tree built entirely throughthe ordinary DOM API —
new DOMImplementation().createDocument(...), then K×createTextNode+appendChildon one parent — reaches the same O(K²) merge when the application callsnormalize(),with no parsing and no error-recovery. The parser is only one of the two callers of the
vulnerable merge:
endDocument()→document.normalize()(the parse-path trigger above), andnormalize()on a tree with adjacent text nodes.XMLSerializerdoes not callnormalize(), so serializing an un-merged tree is O(total text), notO(K²); the O(K²) surface is exactly those two
normalize()callers. Consequently a parser-side boundalone cannot remediate Finding B — the fix must live in
normalize().Affected Versions
Both findings are present across the full published
@xmldom/xmldomhistory — bothcurrently-maintained versions (
0.8.xand0.9.x) are affected — and across the retired unscopedxmldomline. Finding B'snormalize()merge is additionally reachable programmatically: adirect
normalize()call on a DOM built with adjacent text nodes hits the same O(K²) merge,independent of the parser — so, unlike Finding A, it does not require the malformed-input recovery
path.
Proof of Concept
Default
DOMParser, no options. The input is trivially compressible (a</a<>repeated) andnever throws — it is parsed via the recovery path.
Measured on Node v18.20.8 (absolute ms vary by host; the load-bearing fact is that doubling the
input ~quadruples the time — canonical O(n²)):
Finding A, isolated (
"<r>" + "a<"×N + "</r>", normalize disabled to isolate the re-scan):@xmldom/xmldom0.9.10Finding B, isolated (
"<r>" + "a<>"×N + "</r>", time attributable tonormalize()):Combined (default parser, both findings; `"" + "a<"×N