From ae7f13512d6989eefb47c711e62ed0edabd91d0e Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Thu, 8 May 2025 03:18:12 +0000 Subject: [PATCH] Bug 1959727 - Add the sanitizer option to setHTMLUnsafe. r=emilio Differential Revision: https://phabricator.services.mozilla.com/D247109 --- dom/base/Element.cpp | 6 +- dom/base/Element.h | 2 + dom/base/ShadowRoot.cpp | 6 +- dom/base/ShadowRoot.h | 1 + dom/base/nsContentUtils.cpp | 114 +++++++++++++++++- dom/base/nsContentUtils.h | 2 + dom/html/HTMLTemplateElement.cpp | 6 +- dom/html/HTMLTemplateElement.h | 1 + dom/webidl/Element.webidl | 3 +- dom/webidl/Sanitizer.webidl | 6 +- dom/webidl/ShadowRoot.webidl | 2 +- ...nitizer-basic-filtering.tentative.html.ini | 30 ----- 12 files changed, 131 insertions(+), 48 deletions(-) diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp index cec308ac465e..db73c64cfd1a 100644 --- a/dom/base/Element.cpp +++ b/dom/base/Element.cpp @@ -5493,10 +5493,12 @@ EditorBase* Element::GetExtantEditor() const { } void Element::SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError) { - nsContentUtils::SetHTMLUnsafe(this, this, aHTML, false /*aIsShadowRoot*/, - aSubjectPrincipal, aError); + nsContentUtils::SetHTMLUnsafe(this, this, aHTML, aOptions, + false /*aIsShadowRoot*/, aSubjectPrincipal, + aError); } // https://html.spec.whatwg.org/#event-beforematch diff --git a/dom/base/Element.h b/dom/base/Element.h index 2b2b6bd7a30d..a5b30af332de 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -119,6 +119,7 @@ struct URLValue; namespace dom { struct CheckVisibilityOptions; struct CustomElementData; +struct SetHTMLUnsafeOptions; struct SetHTMLOptions; struct GetHTMLOptions; struct GetAnimationsOptions; @@ -2270,6 +2271,7 @@ class Element : public FragmentOrElement { MOZ_CAN_RUN_SCRIPT virtual void SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError); diff --git a/dom/base/ShadowRoot.cpp b/dom/base/ShadowRoot.cpp index d8e6e56cd5d7..23050b58603f 100644 --- a/dom/base/ShadowRoot.cpp +++ b/dom/base/ShadowRoot.cpp @@ -885,11 +885,13 @@ nsresult ShadowRoot::Clone(dom::NodeInfo* aNodeInfo, nsINode** aResult) const { } void ShadowRoot::SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError) { RefPtr host = GetHost(); - nsContentUtils::SetHTMLUnsafe(this, host, aHTML, true /*aIsShadowRoot*/, - aSubjectPrincipal, aError); + nsContentUtils::SetHTMLUnsafe(this, host, aHTML, aOptions, + true /*aIsShadowRoot*/, aSubjectPrincipal, + aError); } void ShadowRoot::GetInnerHTML( diff --git a/dom/base/ShadowRoot.h b/dom/base/ShadowRoot.h index 4830ba7cef11..53490e3486b3 100644 --- a/dom/base/ShadowRoot.h +++ b/dom/base/ShadowRoot.h @@ -252,6 +252,7 @@ class ShadowRoot final : public DocumentFragment, public DocumentOrShadowRoot { MOZ_CAN_RUN_SCRIPT void SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError); // @param aInnerHTML will always be of type `NullIsEmptyString`. diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 2ce63019a2e8..502baf98cc1b 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -197,6 +197,7 @@ #include "mozilla/dom/PContentChild.h" #include "mozilla/dom/PrototypeList.h" #include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/dom/Sanitizer.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/ShadowRoot.h" @@ -5862,13 +5863,107 @@ uint32_t computeSanitizationFlags(nsIPrincipal* aPrincipal, int32_t aFlags) { return sanitizationFlags; } +// https://wicg.github.io/sanitizer-api/#set-and-filter-html +static void SetAndFilterHTML( + FragmentOrElement* aTarget, Element* aContext, const nsAString& aHTML, + const OwningSanitizerOrSanitizerConfigOrSanitizerPresets& aSanitizerOptions, + const bool aSafe, ErrorResult& aError) { + RefPtr doc = aTarget->OwnerDoc(); + + // Step 1. If safe and contextElement’s local name is "script" and + // contextElement’s namespace is the HTML namespace or the SVG namespace, then + // return. + if (aSafe && (aContext->IsHTMLElement(nsGkAtoms::script) || + aContext->IsSVGElement(nsGkAtoms::script))) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, + "SetHTMLScript"); + return; + } + + // Step 2. Let sanitizer be the result of calling get a sanitizer instance + // from options with options and safe. + nsCOMPtr global = aTarget->GetOwnerGlobal(); + if (!global) { + aError.ThrowInvalidStateError("Missing owner global."); + return; + } + RefPtr sanitizer = + Sanitizer::GetInstance(global, aSanitizerOptions, aSafe, aError); + if (aError.Failed()) { + return; + } + + // Batch possible DOMSubtreeModified events. + mozAutoSubtreeModified subtree(doc, nullptr); + + aTarget->FireNodeRemovedForChildren(); + + // Needed when innerHTML is used in combination with contenteditable + mozAutoDocUpdate updateBatch(doc, true); + + // Remove childnodes. + nsAutoMutationBatch mb(aTarget, true, false); + aTarget->RemoveAllChildren(true); + mb.RemovalDone(); + + nsAutoScriptLoaderDisabler sld(doc); + + // Step 3. Let newChildren be the result of the HTML fragment parsing + // algorithm steps given contextElement, html, and true. + // Step 4. Let fragment be a new DocumentFragment whose node document is + // contextElement’s node document. + // Step 5. For each node in newChildren, append node to fragment. + + // We MUST NOT cause any requests during parsing, so we'll + // create an inert Document and parse into a new DocumentFragment. + + RefPtr inertDoc = nsContentUtils::CreateInertHTMLDocument(doc); + if (!inertDoc) { + aError = NS_ERROR_FAILURE; + return; + } + + RefPtr fragment = new (inertDoc->NodeInfoManager()) + DocumentFragment(inertDoc->NodeInfoManager()); + + nsAtom* contextLocalName = aContext->NodeInfo()->NameAtom(); + int32_t contextNameSpaceID = aContext->GetNameSpaceID(); + aError = nsContentUtils::ParseFragmentHTML(aHTML, fragment, contextLocalName, + contextNameSpaceID, false, true); + if (aError.Failed()) { + return; + } + + // Suppress assertion about node removal mutation events that can't have + // listeners anyway, because no one has had the chance to register + // mutation listeners on the fragment that comes from the parser. + nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; + + int32_t oldChildCount = static_cast(aTarget->GetChildCount()); + + // Step 6. Run sanitize on fragment using sanitizer and safe. + sanitizer->Sanitize(fragment, aSafe, aError); + if (aError.Failed()) { + return; + } + + // Step 7. Replace all with fragment within target. + aTarget->AppendChild(*fragment, aError); + if (aError.Failed()) { + return; + } + + mb.NodesAdded(); + nsContentUtils::FireMutationEventsForDirectParsing(doc, aTarget, + oldChildCount); +} + /* static */ -void nsContentUtils::SetHTMLUnsafe(FragmentOrElement* aTarget, - Element* aContext, - const TrustedHTMLOrString& aSource, - bool aIsShadowRoot, - nsIPrincipal* aSubjectPrincipal, - ErrorResult& aError) { +void nsContentUtils::SetHTMLUnsafe( + FragmentOrElement* aTarget, Element* aContext, + const TrustedHTMLOrString& aSource, const SetHTMLUnsafeOptions& aOptions, + bool aIsShadowRoot, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError) { constexpr nsLiteralString elementSink = u"Element setHTMLUnsafe"_ns; constexpr nsLiteralString shadowRootSink = u"ShadowRoot setHTMLUnsafe"_ns; Maybe compliantStringHolder; @@ -5881,6 +5976,13 @@ void nsContentUtils::SetHTMLUnsafe(FragmentOrElement* aTarget, return; } + // Fallback to the more optimized code below without a sanitizer. + if (aOptions.mSanitizer.WasPassed()) { + return SetAndFilterHTML(aTarget, aContext, *compliantString, + aOptions.mSanitizer.Value(), /* aSafe */ false, + aError); + } + RefPtr fragment; { MOZ_ASSERT(!sFragmentParsingActive, diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 729a31e775da..beb6cb220173 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -192,6 +192,7 @@ class MessageBroadcaster; class NodeInfo; class OwningFileOrUSVStringOrFormData; class Selection; +struct SetHTMLUnsafeOptions; enum class ShadowRootMode : uint8_t; class ShadowRoot; struct StructuredSerializeOptions; @@ -1894,6 +1895,7 @@ class nsContentUtils { static void SetHTMLUnsafe(mozilla::dom::FragmentOrElement* aTarget, Element* aContext, const mozilla::dom::TrustedHTMLOrString& aSource, + const mozilla::dom::SetHTMLUnsafeOptions& aOptions, bool aIsShadowRoot, nsIPrincipal* aSubjectPrincipal, mozilla::ErrorResult& aError); /** diff --git a/dom/html/HTMLTemplateElement.cpp b/dom/html/HTMLTemplateElement.cpp index 6f47d863cd2d..e024bf342af9 100644 --- a/dom/html/HTMLTemplateElement.cpp +++ b/dom/html/HTMLTemplateElement.cpp @@ -101,11 +101,13 @@ bool HTMLTemplateElement::ParseAttribute(int32_t aNamespaceID, } void HTMLTemplateElement::SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError) { RefPtr content = mContent; - nsContentUtils::SetHTMLUnsafe(content, this, aHTML, false /*aIsShadowRoot*/, - aSubjectPrincipal, aError); + nsContentUtils::SetHTMLUnsafe(content, this, aHTML, aOptions, + false /*aIsShadowRoot*/, aSubjectPrincipal, + aError); } } // namespace mozilla::dom diff --git a/dom/html/HTMLTemplateElement.h b/dom/html/HTMLTemplateElement.h index 07f2bc1591e8..4eee8d89b589 100644 --- a/dom/html/HTMLTemplateElement.h +++ b/dom/html/HTMLTemplateElement.h @@ -74,6 +74,7 @@ class HTMLTemplateElement final : public nsGenericHTMLElement { MOZ_CAN_RUN_SCRIPT void SetHTMLUnsafe(const TrustedHTMLOrString& aHTML, + const SetHTMLUnsafeOptions& aOptions, nsIPrincipal* aSubjectPrincipal, ErrorResult& aError) final; diff --git a/dom/webidl/Element.webidl b/dom/webidl/Element.webidl index 30d38af20cf3..591f5f176def 100644 --- a/dom/webidl/Element.webidl +++ b/dom/webidl/Element.webidl @@ -405,9 +405,8 @@ dictionary GetHTMLOptions { partial interface Element { // https://html.spec.whatwg.org/#dom-element-sethtmlunsafe - /* TODO: optional SetHTMLUnsafeOptions options = {} */ [NeedsSubjectPrincipal=NonSystem, Throws] - undefined setHTMLUnsafe((TrustedHTML or DOMString) html); + undefined setHTMLUnsafe((TrustedHTML or DOMString) html, optional SetHTMLUnsafeOptions options = {}); DOMString getHTML(optional GetHTMLOptions options = {}); }; diff --git a/dom/webidl/Sanitizer.webidl b/dom/webidl/Sanitizer.webidl index e03fca19f228..267e7775ff96 100644 --- a/dom/webidl/Sanitizer.webidl +++ b/dom/webidl/Sanitizer.webidl @@ -14,11 +14,11 @@ enum SanitizerPresets { "default" }; dictionary SetHTMLOptions { (Sanitizer or SanitizerConfig or SanitizerPresets) sanitizer = "default"; }; -/* dictionary SetHTMLUnsafeOptions { - (Sanitizer or SanitizerConfig or SanitizerPresets) sanitizer = {}; + // TODO: = {}; (Using optional to easily detect a missing sanitizer) + [Pref="dom.security.sanitizer.enabled"] + (Sanitizer or SanitizerConfig or SanitizerPresets) sanitizer; }; -*/ dictionary SanitizerElementNamespace { required DOMString name; diff --git a/dom/webidl/ShadowRoot.webidl b/dom/webidl/ShadowRoot.webidl index 8858b0de0b30..c3c905bdd591 100644 --- a/dom/webidl/ShadowRoot.webidl +++ b/dom/webidl/ShadowRoot.webidl @@ -60,7 +60,7 @@ interface ShadowRoot : DocumentFragment partial interface ShadowRoot { // https://html.spec.whatwg.org/#dom-shadowroot-sethtmlunsafe [NeedsSubjectPrincipal=NonSystem, Throws] - undefined setHTMLUnsafe((TrustedHTML or DOMString) html); + undefined setHTMLUnsafe((TrustedHTML or DOMString) html, optional SetHTMLUnsafeOptions options = {}); DOMString getHTML(optional GetHTMLOptions options = {}); }; diff --git a/testing/web-platform/meta/sanitizer-api/sanitizer-basic-filtering.tentative.html.ini b/testing/web-platform/meta/sanitizer-api/sanitizer-basic-filtering.tentative.html.ini index 3e9e3f821614..9fb3d7ad24bb 100644 --- a/testing/web-platform/meta/sanitizer-api/sanitizer-basic-filtering.tentative.html.ini +++ b/testing/web-platform/meta/sanitizer-api/sanitizer-basic-filtering.tentative.html.ini @@ -1,43 +1,25 @@ [sanitizer-basic-filtering.tentative.html] - [setHTMLUnsafe testcase elements/1, "

Hello World!"] - expected: FAIL - [parseHTML testcase elements/1, "

Hello World!"] expected: FAIL [parseHTMLUnsafe testcase elements/1, "

Hello World!"] expected: FAIL - [setHTMLUnsafe testcase elements/2, "

Hello World!"] - expected: FAIL - [parseHTML testcase elements/2, "

Hello World!"] expected: FAIL [parseHTMLUnsafe testcase elements/2, "

Hello World!"] expected: FAIL - [setHTMLUnsafe testcase elements/3, "

Hello World!"] - expected: FAIL - [parseHTMLUnsafe testcase elements/3, "

Hello World!"] expected: FAIL - [setHTMLUnsafe testcase elements/4, "

Hello World!"] - expected: FAIL - [parseHTMLUnsafe testcase elements/4, "

Hello World!"] expected: FAIL - [setHTMLUnsafe testcase attributes/1, "

x"] - expected: FAIL - [parseHTMLUnsafe testcase attributes/1, "

x"] expected: FAIL - [setHTMLUnsafe testcase attributes/2, "

x"] - expected: FAIL - [parseHTMLUnsafe testcase attributes/2, "

x"] expected: FAIL @@ -50,18 +32,12 @@ [parseHTMLUnsafe testcase attributes-per-element/0, "

"] expected: FAIL - [setHTMLUnsafe testcase attributes-per-element/1, "
"] - expected: FAIL - [parseHTML testcase attributes-per-element/1, "
"] expected: FAIL [parseHTMLUnsafe testcase attributes-per-element/1, "
"] expected: FAIL - [setHTMLUnsafe testcase comments/1, "a b"] - expected: FAIL - [parseHTMLUnsafe testcase comments/1, "a b"] expected: FAIL @@ -89,9 +65,6 @@ [setHTML testcase namespaces/0, "x"] expected: FAIL - [setHTMLUnsafe testcase namespaces/1, ""] - expected: FAIL - [parseHTML testcase namespaces/1, ""] expected: FAIL @@ -113,9 +86,6 @@ [parseHTMLUnsafe testcase namespaces/3, ""] expected: FAIL - [setHTMLUnsafe testcase namespaces/4, "x"] - expected: FAIL - [parseHTML testcase namespaces/4, "x"] expected: FAIL