CVE-2026-12309
Overview
Background
- `StartElement`
- internal serializer routine in
nsContentUtils.cppthat writes an element’s start tag (name and attributes) into aStringBuilderduring HTML fragment serialization. - `is` attribute
- the custom-element attribute whose value names a customized built-in element; here it is synthesized from custom-element data (
ceData->GetIs) rather than read from a real DOM attribute. - `StringBuilder`
- the serializer’s output accumulator whose length must be reserved up front to match exactly what is later written, so an under-reserved append breaks the length/write invariant.
- Attribute-value encoding
- the escaping of characters such as
",&, and non-breaking space in an attribute value so the serialized markup re-parses back to the same value.
Root Cause Analysis
When serializing an element that carries custom-element is data but no explicit is DOM attribute, StartElement appended the synthesized isAttr atom directly via aBuilder.Append(isAttr), bypassing the attribute-value encoding path that every other attribute value flows through in AppendEncodedAttributeValue. This violated the invariant that any value emitted inside is="..." must be run through ExtraSpaceNeededForAttrEncoding and AppendWithAttrEncode so that special characters are escaped and the StringBuilder’s reserved capacity matches the bytes written. Because the raw atom was appended without encoding, a value containing characters needing escaping could be emitted verbatim, corrupting the serialized output and desynchronizing the builder’s precomputed length from its actual content.
The fix extracts the correct encode-or-fast-path logic into the new AppendEncodedAtomAttributeValue helper and routes both AppendEncodedAttributeValue and the is-attribute branch of StartElement through it. This works because the is value now takes the same validated, capacity-accounted encoding path as all other attribute values.
is attribute value was appended raw while all other attribute values were attribute-encoded, leaving one serialization path unescaped and unaccounted for in the builder’s reserved length. The fix unifies both paths behind AppendEncodedAtomAttributeValue, so the is value is encoded and space-reserved identically to every other attribute.Attack Path
- Craft a customized built-in element
Script creates or configures an element whose custom-element
isvalue contains characters that require attribute encoding. - Trigger serialization
Cause the element to be serialized to HTML (for example via
innerHTML/outerHTMLread or fragment serialization), reaching theis-attribute branch ofStartElement. - Emit the unencoded value
The raw atom is appended without escaping, so the serialized
is="..."no longer round-trips and the builder’s written length diverges from the reserved length. - Re-parse the corrupted markup Feeding the malformed serialization back through the parser yields markup that differs from the original DOM, enabling a mutation-based mismatch.
Impact Assessment
is value and cause it to be serialized gains a serialization/parse mismatch and, per the metadata, a memory-safety defect from the mismatched StringBuilder length accounting, all within the content process. The impact is rated moderate/medium, and it requires the ability to run script or supply content that reaches the affected serialization path. No CVSS or exploit primitive beyond this is supported by the diff.Files Changed
dom/base/nsContentUtils.cpp
Audit Directions
- Duplicated encoding logicSearch serializers for attribute or text values appended via a raw
Appendthat skip the sharedAppendWithAttrEncode/ExtraSpaceNeededForAttrEncodingpath, since divergent copies drift out of sync. - Synthesized attributesAudit values injected from internal state (like
ceData->GetIs) rather than real DOM attributes, as these often bypass the encoding that genuine attribute values receive. - Length-reservation mismatchesReview
StringBuilderproducers where reserved capacity is computed separately from the bytes written, ensuring every branch that writes also accounts for its length.
Patch
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index 9e696280346..392d88c6171 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -10797,19 +10797,24 @@ static CheckedInt<uint32_t> ExtraSpaceNeededForAttrEncoding(
return CheckedInt<uint32_t>(numEncodedChars) * maxCharExtraSpace;
}
+static void AppendEncodedAtomAttributeValue(nsAtom* aAtom,
+ StringBuilder& aBuilder) {
+ nsDependentAtomString atomStr(aAtom);
+ auto space = ExtraSpaceNeededForAttrEncoding(atomStr);
+ if (space.isValid() && !space.value()) {
+ aBuilder.Append(aAtom);
+ } else {
+ aBuilder.AppendWithAttrEncode(nsString(atomStr), space + atomStr.Length());
+ }
+}
+
static void AppendEncodedAttributeValue(const nsAttrValue& aValue,
StringBuilder& aBuilder) {
if (nsAtom* atom = aValue.GetStoredAtom()) {
- nsDependentAtomString atomStr(atom);
- auto space = ExtraSpaceNeededForAttrEncoding(atomStr);
- if (space.isValid() && !space.value()) {
- aBuilder.Append(atom);
- } else {
- aBuilder.AppendWithAttrEncode(nsString(atomStr),
- space + atomStr.Length());
- }
+ AppendEncodedAtomAttributeValue(atom, aBuilder);
return;
}
+
// NOTE(emilio): In most cases this will just be a reference to the stored
// nsStringBuffer.
nsString str;
@@ -10838,7 +10843,7 @@ static void StartElement(Element* aElement, StringBuilder& aBuilder) {
nsAtom* isAttr = ceData->GetIs(aElement);
if (isAttr && !aElement->HasAttr(nsGkAtoms::is)) {
aBuilder.Append(uR"( is=")");
- aBuilder.Append(isAttr);
+ AppendEncodedAtomAttributeValue(isAttr, aBuilder);
aBuilder.Append(uR"(")");
}
}