CVE-2026-12314
Overview
Background
- `nsHtml5TreeOperation`
- the class that applies queued HTML5 parser tree-building operations (append, create element, etc.) to the live DOM.
- `AutoSetThrowOnDynamicMarkupInsertionCounter`
- an RAII guard that increments a per-
Documentcounter which makes reentrant dynamic markup insertion (e.g.document.write) throw instead of mutating the tree. - `AutoCEReaction`
- an RAII scope that drives the custom element reaction stack, causing queued custom element constructors and reaction callbacks to execute when it unwinds.
- `nsHtml5AutoPauseUpdate`
- an RAII guard that pauses the parser’s content-update batching while script (such as a custom element constructor) is allowed to run.
Root Cause Analysis
When nsHtml5TreeOperation::Append inserted a node whose parent’s DocGroup had a custom element queue pushed for the current recursion depth, it emplaced an AutoCEReaction and nsHtml5AutoPauseUpdate so custom element constructors would run, but it never established an AutoSetThrowOnDynamicMarkupInsertionCounter on the document. This violated the invariant that any code path running custom element script during HTML5 parsing must forbid reentrant dynamic markup insertion, so a constructor could call document.write/document.open and reenter the parser while its tree-operation state was still live. Separately, in nsHtml5TreeOperation::CreateHTMLElement the guard was constructed against the local document variable rather than the parser builder’s document (aBuilder->GetDocument()), so when those two documents differed the guard protected the wrong document and left the parser’s actual document open to reentrancy.
The fix adds the missing Maybe<AutoSetThrowOnDynamicMarkupInsertionCounter> in Append, emplacing it on aBuilder->GetDocument() before the reaction/pause guards, and repoints the existing guard in CreateHTMLElement to aBuilder->GetDocument(). This works because the throw-on-insertion counter is now raised on the exact document the parser is mutating whenever custom element script can run, so reentrant markup insertion throws instead of corrupting parser state.
Maybe-wrapped guard in Append and a corrected document reference in CreateHTMLElement) so any reentrant markup insertion is rejected rather than mutating a document the parser assumes is stable.Attack Path
- Register a custom element The attacker’s page defines a custom element whose constructor or reaction callback runs attacker-controlled script.
- Trigger reactive parsing
Markup that causes the HTML5 parser to append/create an instance of that element enqueues a custom element reaction during
nsHtml5TreeOperationprocessing. - Run script mid-operation
As
AutoCEReactionunwinds, the constructor runs while the parser’s tree-operation state is still live and unprotected. - Reenter the parser
The constructor calls a dynamic markup insertion API (e.g.
document.write) that would normally be blocked, mutating or tearing down the document the parser is operating on. - Corrupt parser state The parser resumes against a now-inconsistent document, leading to a memory-safety violation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifparser/html/nsHtml5TreeOperation.cpp |
modified |
Files Changed
parser/html/nsHtml5TreeOperation.cpp
Audit Directions
- Custom element reaction scopesAudit every site that emplaces
AutoCEReactionornsHtml5AutoPauseUpdateduring parsing to confirm anAutoSetThrowOnDynamicMarkupInsertionCounteris armed on the same document before script can run. - Document-reference mismatchesLook for guards or counters constructed against a locally-derived
documentwhen a builder/parser exposes its ownGetDocument(), since the two can diverge and leave the wrong document protected. - Parser reentrancy invariantsReview other
nsHtml5TreeOperationpaths that can invoke script (constructors, callbacks, mutation observers) for missing protection against reentrantdocument.write/document.open.
Patch
diff --git a/parser/html/nsHtml5TreeOperation.cpp b/parser/html/nsHtml5TreeOperation.cpp
index 50e386c8fce..afee91c2050 100644
--- a/parser/html/nsHtml5TreeOperation.cpp
+++ b/parser/html/nsHtml5TreeOperation.cpp
@@ -296,6 +296,8 @@ nsresult nsHtml5TreeOperation::Append(nsIContent* aNode, nsIContent* aParent,
return NS_OK;
}
+ Maybe<AutoSetThrowOnDynamicMarkupInsertionCounter>
+ throwOnDynamicMarkupInsertionCounter;
Maybe<nsHtml5AutoPauseUpdate> autoPause;
Maybe<AutoCEReaction> autoCEReaction;
DocGroup* docGroup = aParent->OwnerDoc()->GetDocGroup();
@@ -308,6 +310,7 @@ nsresult nsHtml5TreeOperation::Append(nsIContent* aNode, nsIContent* aParent,
if (autoCEReaction.isSome() && docGroup &&
docGroup->CustomElementReactionsStack()
->IsElementQueuePushedForCurrentRecursionDepth()) {
+ throwOnDynamicMarkupInsertionCounter.emplace(aBuilder->GetDocument());
autoPause.emplace(aBuilder);
}
return rv;
@@ -582,7 +585,7 @@ nsIContent* nsHtml5TreeOperation::CreateHTMLElement(
if (customElementDefinition) {
// This will cause custom element constructors to run.
AutoSetThrowOnDynamicMarkupInsertionCounter
- throwOnDynamicMarkupInsertionCounter(document);
+ throwOnDynamicMarkupInsertionCounter(aBuilder->GetDocument());
nsHtml5AutoPauseUpdate autoPauseContentUpdate(aBuilder);
{
nsAutoMicroTask mt;