Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionMemory safety bug fixed in Firefox 152
ComponentDOM
Bug ClassLogic Error
Tracker2007083
Fix commit0e852ba43ace (firefox) +58/-1
CISA KEVNot listed
CreditedFrédéric Wang Nélar
Disclosed2026-06-16

Background

CSP nonce
a per-load random token in a Content-Security-Policy that whitelists an inline script or style so it may execute.
Nonce hiding
Gecko’s practice of moving an element’s nonce value into an internal slot and clearing the DOM attribute so it cannot be read back after parsing.
`MathMLElement`
the C++ DOM class (dom/mathml/MathMLElement.cpp) backing MathML elements such as <math>, which previously lacked nonce-hiding logic.
CSS selector exfiltration
an attack that reads a secret attribute value character-by-character via CSS attribute selectors (for example [nonce^="a"]) that trigger observable side effects like background image fetches.

Root Cause Analysis

MathMLElement did not implement the nonce-hiding that HTMLElement and SVGElement already perform, so a nonce attribute set on a MathML element remained readable as a live DOM attribute after binding. When a document’s CSP was delivered through an HTTP header, an attacker with markup-injection but no script capability could inject a MathML element carrying the page’s valid nonce and then use CSS attribute selectors to leak the nonce value character-by-character, violating the invariant that a CSP nonce must never be observable through the DOM or CSS.

The fix adds an internal nonce property slot (SetNonce/GetNonce/RemoveNonce), captures the attribute value in AfterSetAttr, and, when NODE_HAS_NONCE_AND_HEADER_CSP is set and the element is in a composed document, schedules a script runner in BindToTree that copies the nonce internally and clears the DOM attribute. CopyInnerTo is added so cloning preserves the internal nonce slot rather than resurrecting an exposed attribute. This closes the exfiltration path because the reflected attribute is emptied while the effective nonce is retained only in the non-observable internal slot.

Key insight
The single mistake was that MathMLElement was omitted from the nonce-hiding treatment applied to HTML and SVG elements, leaving the CSP nonce readable as a plain DOM attribute; the fix ports the identical internal-slot-plus-attribute-clearing mechanism (and clone handling) to MathML.

Attack Path

  1. Deliver header CSP The victim page ships a nonce-based Content-Security-Policy via an HTTP response header, setting GetHasCSPDeliveredThroughHeader.
  2. Inject MathML with nonce An attacker with HTML-injection (but not script execution) inserts a MathML element carrying a nonce attribute equal to the page’s valid nonce.
  3. Probe via CSS selectors The attacker uses CSS attribute selectors like math[nonce^="x"] combined with side-effecting rules to test each candidate character of the nonce.
  4. Reconstruct the nonce Repeated probing leaks the nonce one character at a time until the full value is recovered.
  5. Bypass CSP With the stolen nonce the attacker crafts inline scripts or styles that CSP now trusts, defeating the policy.

Impact Assessment

An attacker who can inject markup into a page protected by a header-delivered nonce CSP could exfiltrate the CSP nonce and thereby bypass the content-security policy that was meant to block injected script or style. This operates in the content process within the victim’s document context, and requires the preconditions of a header-delivered nonce CSP plus a markup-injection foothold. The rated severity is low.

Changed Functions

FunctionChangeNotes
if
dom/mathml/MathMLElement.cpp
modified
if
dom/mathml/MathMLElement.h
modified

Files Changed

  • dom/mathml/MathMLElement.cpp
  • dom/mathml/MathMLElement.h
  • dom/webidl/MathMLElement.webidl

Audit Directions

  • Nonce-hiding parity
    Verify every element type that can carry a nonce (HTMLElement, SVGElement, MathMLElement) implements the internal-slot capture, BindToTree clearing, and CopyInnerTo retention consistently.
  • Secret attributes in CSS reach
    Audit any security-sensitive attribute that stays live in the DOM for exposure to CSS attribute-selector exfiltration and ensure it is moved to an internal slot.
  • Clone and adopt paths
    Check that CopyInnerTo, Clone, and adoption/move flows preserve internal secret slots without reflecting them back to observable attributes.
diff --git a/dom/mathml/MathMLElement.cpp b/dom/mathml/MathMLElement.cpp
index 06b11a19d5e..9819d774def 100644
--- a/dom/mathml/MathMLElement.cpp
+++ b/dom/mathml/MathMLElement.cpp
@@ -65,6 +65,20 @@ nsresult MathMLElement::BindToTree(BindContext& aContext, nsINode& aParent) {
 
   Link::BindToTree(aContext);
 
+  // Hide any nonce from the DOM, but keep the internal value of the
+  // nonce by copying and resetting the internal nonce value.
+  if (!aContext.IsMove() && HasFlag(NODE_HAS_NONCE_AND_HEADER_CSP) &&
+      IsInComposedDoc() && OwnerDoc()->GetBrowsingContext()) {
+    nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+        "MathMLElement::ResetNonce::Runnable",
+        [self = RefPtr<MathMLElement>(this)]() {
+          nsAutoString nonce;
+          self->GetNonce(nonce);
+          self->SetAttr(kNameSpaceID_None, nsGkAtoms::nonce, u""_ns, true);
+          self->SetNonce(nonce);
+        }));
+  }
+
   // Set the bit in the document for telemetry.
   if (Document* doc = aContext.GetComposedDoc()) {
     doc->SetUseCounter(eUseCounter_custom_MathMLUsed);
@@ -642,6 +656,20 @@ nsresult MathMLElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
 
 NS_IMPL_ELEMENT_CLONE(MathMLElement)
 
+nsresult MathMLElement::CopyInnerTo(mozilla::dom::Element* aDest) {
+  nsresult rv = Element::CopyInnerTo(aDest);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  auto* dest = static_cast<MathMLElement*>(aDest);
+
+  // cloning a node must retain its internal nonce slot
+  if (auto* nonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce))) {
+    dest->SetNonce(*nonce);
+  }
+
+  return NS_OK;
+}
+
 void MathMLElement::SetIncrementScriptLevel(bool aIncrementScriptLevel,
                                             bool aNotify) {
   NS_ASSERTION(aNotify, "We always notify!");
@@ -757,6 +785,20 @@ void MathMLElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
     }
   }
 
+  // The nonce will be copied over to an internal slot and cleared from the
+  // Element within BindToTree to avoid CSS Selector nonce exfiltration if
+  // the CSP list contains a header-delivered CSP.
+  if (nsGkAtoms::nonce == aName && kNameSpaceID_None == aNameSpaceID) {
+    if (aValue) {
+      SetNonce(nsAttrValueOrString(aValue).String());
+      if (OwnerDoc()->GetHasCSPDeliveredThroughHeader()) {
+        SetFlags(NODE_HAS_NONCE_AND_HEADER_CSP);
+      }
+    } else {
+      RemoveNonce();
+    }
+  }
+
   return MathMLElementBase::AfterSetAttr(aNameSpaceID, aName, aValue, aOldValue,
                                          aSubjectPrincipal, aNotify);
 }
diff --git a/dom/mathml/MathMLElement.h b/dom/mathml/MathMLElement.h
index f8daeb943b2..4b0846796ad 100644
--- a/dom/mathml/MathMLElement.h
+++ b/dom/mathml/MathMLElement.h
@@ -32,6 +32,18 @@ class MathMLElement final : public MathMLElementBase, public Link {
 
   NS_IMPL_FROMNODE(MathMLElement, kNameSpaceID_MathML)
 
+  void SetNonce(const nsAString& aNonce) {
+    SetProperty(nsGkAtoms::nonce, new nsString(aNonce),
+                nsINode::DeleteProperty<nsString>, /* aTransfer = */ true);
+  }
+  void RemoveNonce() { RemoveProperty(nsGkAtoms::nonce); }
+  void GetNonce(nsAString& aNonce) const {
+    nsString* cspNonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce));
+    if (cspNonce) {
+      aNonce = *cspNonce;
+    }
+  }
+
   nsresult BindToTree(BindContext&, nsINode& aParent) override;
   void UnbindFromTree(UnbindContext&) override;
 
@@ -66,6 +78,7 @@ class MathMLElement final : public MathMLElementBase, public Link {
   MOZ_CAN_RUN_SCRIPT
   nsresult PostHandleEvent(mozilla::EventChainPostVisitor& aVisitor) override;
   nsresult Clone(mozilla::dom::NodeInfo*, nsINode** aResult) const override;
+  nsresult CopyInnerTo(mozilla::dom::Element* aDest);
 
   // Set during reflow as necessary. Does a style change notification,
   // aNotify must be true.
diff --git a/dom/webidl/MathMLElement.webidl b/dom/webidl/MathMLElement.webidl
index 882a3d1d32a..b51eb392733 100644
--- a/dom/webidl/MathMLElement.webidl
+++ b/dom/webidl/MathMLElement.webidl
@@ -10,7 +10,9 @@
  */
 
 [Exposed=Window]
-interface MathMLElement : Element { };
+interface MathMLElement : Element {
+  attribute DOMString nonce;
+};
 MathMLElement includes GlobalEventHandlers;
 MathMLElement includes HTMLOrSVGOrMathMLElement;
 MathMLElement includes ElementCSSInlineStyle;
Loading diff…