CVE-2026-12317
Overview
Background
- CSP nonce
- a per-load random token in a
Content-Security-Policythat whitelists an inline script or style so it may execute. - Nonce hiding
- Gecko’s practice of moving an element’s
noncevalue 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.
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
- Deliver header CSP
The victim page ships a nonce-based
Content-Security-Policyvia an HTTP response header, settingGetHasCSPDeliveredThroughHeader. - Inject MathML with nonce
An attacker with HTML-injection (but not script execution) inserts a MathML element carrying a
nonceattribute equal to the page’s valid nonce. - 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. - Reconstruct the nonce Repeated probing leaks the nonce one character at a time until the full value is recovered.
- Bypass CSP With the stolen nonce the attacker crafts inline scripts or styles that CSP now trusts, defeating the policy.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/mathml/MathMLElement.cpp |
modified | |
ifdom/mathml/MathMLElement.h |
modified |
Files Changed
dom/mathml/MathMLElement.cppdom/mathml/MathMLElement.hdom/webidl/MathMLElement.webidl
Audit Directions
- Nonce-hiding parityVerify every element type that can carry a
nonce(HTMLElement,SVGElement,MathMLElement) implements the internal-slot capture,BindToTreeclearing, andCopyInnerToretention consistently. - Secret attributes in CSS reachAudit 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 pathsCheck that
CopyInnerTo,Clone, and adoption/move flows preserve internal secret slots without reflecting them back to observable attributes.
Patch
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;