Moderate firefox Policy Bypass 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMitigation bypass in the DOM: Security component
ComponentDOM
Bug ClassPolicy Bypass
Tracker2075930
Fix commit2f954d793c28 (firefox) +13/-5
CISA KEVNot listed
CreditedNguyen Tuan Hung
Disclosed2026-10-27

Background

`nsTreeSanitizer`
Firefox’s HTML/XML sanitizer (dom/base/nsTreeSanitizer.cpp) used by nsIParserUtils::sanitize, Sanitizer/setHTML, and fragment parsing to strip disallowed elements, attributes, and scripting constructs from untrusted markup.
Customized built-in elements / the `is` attribute
HTML lets an author upgrade a built-in element to a customized built-in via is="..." (e.g. <style is="custom-style">). The upgrade is recorded as CustomElementData attached to the element, retrievable with GetCustomElementData()/HasCustomElementData().
`CustomElementData`
Per-element state tracking the custom-element registration (including the is name via GetIs()); if left on a sanitized element it can later cause the element to be treated as a customized built-in and run author-defined reactions.
`SanitizeAttributes` vs `SanitizeChildren`
SanitizeChildren(nsINode*) walks the subtree and decides which elements survive; SanitizeAttributes(Element*, AllowedAttributes) strips disallowed attributes on a kept element. Not every kept element flows through the same point in SanitizeChildren’s per-namespace branch.
Mitigation bypass
Advisory class: the sanitizer is a security mitigation, and a construct surviving it that should have been neutralized is a bypass.

Root Cause Analysis

The patch relocates the clearing of CustomElementData. Previously, SanitizeChildren(nsINode* aRoot) cleared custom-element data inline, immediately before resolving the per-namespace AllowedAttributes for an element in the XHTML/MathML/SVG branch:

if (elt->HasCustomElementData()) {
  MOZ_ASSERT(elt->GetCustomElementData()->GetIs(elt), ...);
  elt->ClearCustomElementData();
}

The defect is that this clearing lived in only one code path of the sanitizer, so elements that reach attribute sanitization through other routes — or elements like <style> that are kept and attribute-sanitized but not neutralized along that exact branch — could retain their CustomElementData from an is="..." attribute even after the sanitizer removed the is attribute itself. An element left carrying custom-element data is still treated as a customized built-in, re-enabling customized-built-in behavior on sanitized output and defeating the sanitizer. The patch moves the identical clearing block into SanitizeAttributes at its very start, so that every element whose attributes are sanitized has its custom-element data cleared, regardless of which caller reached it. The accompanying parser test adds <style is="custom-style"> with SanitizerAllowStyle, confirming the <style> upgrade is stripped to a plain <style>. The advisory’s generic “mitigation bypass in DOM: Security” is consistent with the diff: this is a sanitizer bypass (PolicyBypass), not a memory-safety bug.

Key insight
Custom-element data from the is attribute was cleared in only one branch of the tree walk, so elements attribute-sanitized through another path kept their customized-built-in state and survived the sanitizer as live custom elements; the fix moves the clear into SanitizeAttributes, the choke point every kept element passes through.

Attack Path

  1. Attacker supplies markup Attacker controls an HTML string that will be run through nsTreeSanitizer (e.g. via a sanitization API, pasted content, or an embedding that sanitizes before insertion).
  2. Embed a customized built-in Include an element such as <style is="custom-style"> (or another element reaching attribute sanitization on a path that did not clear custom-element data).
  3. Bypass the clear The is attribute is stripped, but the element retains its CustomElementData because the pre-fix clear in SanitizeChildren did not cover this element.
  4. Sanitized output stays a custom element The surviving element is still a customized built-in in the output document.
  5. Primitive achieved Custom-element reactions/registration tied to the is name apply to content that was supposed to be inert sanitized markup, bypassing the sanitizer mitigation (any script execution depends on a matching registration, inferred).

Impact Assessment

This is a renderer-side logic/policy bug requiring no elevated privilege: any content that gets sanitized by nsTreeSanitizer is a candidate. The attacker gains a sanitizer bypass — markup that should be neutralized retains customized-built-in state. Whether this escalates to script execution depends on the presence of a matching custom-element definition and the sink that consumes the sanitized output (inferred, not shown by the diff); the diff itself only proves the custom-element data was not cleared. Mozilla rates it moderate. The primitive is best described as a mitigation/sanitizer bypass rather than direct memory corruption.

Changed Functions

FunctionChangeNotes
for
dom/base/nsTreeSanitizer.cpp
modified
if
dom/base/nsTreeSanitizer.cpp
modified

Files Changed

  • dom/base/nsTreeSanitizer.cpp
  • parser/xml/test/unit/results.js

Audit Directions

  • Per-branch vs choke-point security actions
    Look for other sanitizer actions placed inside one namespace branch of SanitizeChildren that should apply to every kept element; moving security-critical clears to SanitizeAttributes-style choke points is the pattern here.
  • CustomElementData persistence
    Audit other DOM mutation/cloning/adoption paths (fragment parsing, importNode, template instantiation) that keep elements but might not clear stale CustomElementData when the is attribute is removed.
  • Element kept but not fully neutralized
    Review elements that nsTreeSanitizer keeps (style, templates, SVG/MathML specials) for other residual customized-built-in or scripting state surviving sanitization.
diff --git a/dom/base/nsTreeSanitizer.cpp b/dom/base/nsTreeSanitizer.cpp
index 1d4bedcb65f..be4d2fbc184 100644
--- a/dom/base/nsTreeSanitizer.cpp
+++ b/dom/base/nsTreeSanitizer.cpp
@@ -1183,6 +1183,13 @@ static bool UTF16StringStartsWith(const char16_t* aStr, uint32_t aLength,
 
 void nsTreeSanitizer::SanitizeAttributes(mozilla::dom::Element* aElement,
                                          AllowedAttributes aAllowed) {
+  // Clear the custom element data introduced by the "is" attribute.
+  if (aElement->HasCustomElementData()) {
+    MOZ_ASSERT(aElement->GetCustomElementData()->GetIs(aElement),
+               "CustomElementData without an |is| attribute?");
+    aElement->ClearCustomElementData();
+  }
+
   int32_t ac = (int)aElement->GetAttrCount();
 
   for (int32_t i = ac - 1; i >= 0; --i) {
@@ -1451,11 +1458,6 @@ void nsTreeSanitizer::SanitizeChildren(nsINode* aRoot) {
       NS_ASSERTION(ns == kNameSpaceID_XHTML || ns == kNameSpaceID_SVG ||
                        ns == kNameSpaceID_MathML,
                    "Should have only HTML, MathML or SVG here!");
-      if (elt->HasCustomElementData()) {
-        MOZ_ASSERT(elt->GetCustomElementData()->GetIs(elt),
-                   "CustomElementData without an |is| attribute?");
-        elt->ClearCustomElementData();
-      }
       AllowedAttributes allowed;
       if (ns == kNameSpaceID_XHTML) {
         allowed.mNames = sAttributesHTML;
diff --git a/parser/xml/test/unit/results.js b/parser/xml/test/unit/results.js
index b71adfb6aff..b71c7925370 100644
--- a/parser/xml/test/unit/results.js
+++ b/parser/xml/test/unit/results.js
@@ -945,4 +945,10 @@ var vectors = [
     data: `<div is="custom-div">foo</div>`,
     sanitized: "<html><head></head><body><div>foo</div></body></html>",
   },
+  {
+    data: `<body><style is="custom-style">* { color: red; }</style></body>`,
+    flags: Ci.nsIParserUtils.SanitizerAllowStyle,
+    sanitized:
+      "<html><head></head><body><style>* { color: red; }</style></body></html>",
+  },
 ];
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/parser/xml/test/unit/results.js b/parser/xml/test/unit/results.js
index b71adfb6aff..b71c7925370 100644
--- a/parser/xml/test/unit/results.js
+++ b/parser/xml/test/unit/results.js
@@ -945,4 +945,10 @@ var vectors = [
     data: `<div is="custom-div">foo</div>`,
     sanitized: "<html><head></head><body><div>foo</div></body></html>",
   },
+  {
+    data: `<body><style is="custom-style">* { color: red; }</style></body>`,
+    flags: Ci.nsIParserUtils.SanitizerAllowStyle,
+    sanitized:
+      "<html><head></head><body><style>* { color: red; }</style></body></html>",
+  },
 ];
Loading diff…