1
0
Fork 0
firefox/dom/security/sanitizer/Sanitizer.cpp
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

1192 lines
43 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "Sanitizer.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DocumentFragment.h"
#include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/SanitizerBinding.h"
#include "mozilla/dom/SanitizerDefaultConfig.h"
#include "nsContentUtils.h"
#include "nsGenericHTMLElement.h"
#include "nsIContentInlines.h"
#include "nsNameSpaceManager.h"
namespace mozilla::dom {
using namespace sanitizer;
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Sanitizer, mGlobal)
NS_IMPL_CYCLE_COLLECTING_ADDREF(Sanitizer)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Sanitizer)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Sanitizer)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
// Map[ElementName -> ?Set[Attributes]]
using ElementsWithAttributes =
nsTHashMap<const nsStaticAtom*, UniquePtr<StaticAtomSet>>;
StaticAutoPtr<ElementsWithAttributes> sDefaultHTMLElements;
StaticAutoPtr<ElementsWithAttributes> sDefaultMathMLElements;
StaticAutoPtr<StaticAtomSet> sDefaultAttributes;
JSObject* Sanitizer::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return Sanitizer_Binding::Wrap(aCx, this, aGivenProto);
}
/* static */
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-get-a-sanitizer-instance-from-options
already_AddRefed<Sanitizer> Sanitizer::GetInstance(
nsIGlobalObject* aGlobal,
const OwningSanitizerOrSanitizerConfigOrSanitizerPresets& aOptions,
bool aSafe, ErrorResult& aRv) {
// Step 4. If sanitizerSpec is a string:
if (aOptions.IsSanitizerPresets()) {
// Step 4.1. Assert: sanitizerSpec is "default"
MOZ_ASSERT(aOptions.GetAsSanitizerPresets() == SanitizerPresets::Default);
// Step 4.2. Set sanitizerSpec to the built-in safe default configuration.
// NOTE: The built-in safe default configuration is complete and not
// influenced by |safe|.
RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);
sanitizer->SetDefaultConfig();
return sanitizer.forget();
}
// Step 5. Assert: sanitizerSpec is either a Sanitizer instance, or a
// dictionary. Step 6. If sanitizerSpec is a dictionary:
if (aOptions.IsSanitizerConfig()) {
// Step 6.1. Let sanitizer be a new Sanitizer instance.
RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);
// Step 6.2. Let setConfigurationResult be the result of set a configuration
// with sanitizerSpec and not safe on sanitizer.
sanitizer->SetConfig(aOptions.GetAsSanitizerConfig(), !aSafe, aRv);
// Step 6.3. If setConfigurationResult is false, throw a TypeError.
if (aRv.Failed()) {
return nullptr;
}
// Step 6.4. Set sanitizerSpec to sanitizer.
return sanitizer.forget();
}
// Step 7. Assert: sanitizerSpec is a Sanitizer instance.
MOZ_ASSERT(aOptions.IsSanitizer());
// Step 8. Return sanitizerSpec.
RefPtr<Sanitizer> sanitizer = aOptions.GetAsSanitizer();
return sanitizer.forget();
}
/* static */
// https://wicg.github.io/sanitizer-api/#sanitizer-constructor
already_AddRefed<Sanitizer> Sanitizer::Constructor(
const GlobalObject& aGlobal,
const SanitizerConfigOrSanitizerPresets& aConfig, ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<Sanitizer> sanitizer = new Sanitizer(global);
// Step 1. If configuration is a SanitizerPresets string, then:
if (aConfig.IsSanitizerPresets()) {
// Step 1.1. Assert: configuration is default.
MOZ_ASSERT(aConfig.GetAsSanitizerPresets() == SanitizerPresets::Default);
// Step 1.2. Set configuration to the built-in safe default configuration .
sanitizer->SetDefaultConfig();
// NOTE: Early return because we don't need to do any
// processing/verification of the default config.
return sanitizer.forget();
}
// Step 2. Let valid be the return value of set a configuration with
// configuration and true on this.
sanitizer->SetConfig(aConfig.GetAsSanitizerConfig(), true, aRv);
// Step 3. If valid is false, then throw a TypeError.
if (aRv.Failed()) {
return nullptr;
}
return sanitizer.forget();
}
void Sanitizer::SetDefaultConfig() {
MOZ_ASSERT(NS_IsMainThread());
AssertNoLists();
mIsDefaultConfig = true;
// https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration
// {
// ...
// "comments": false,
// "dataAttributes": false
// }
MOZ_ASSERT(!mComments);
MOZ_ASSERT(!mDataAttributes);
if (sDefaultHTMLElements) {
// Already initialized.
return;
}
sDefaultHTMLElements =
new ElementsWithAttributes(std::size(kDefaultHTMLElements));
size_t i = 0;
for (nsStaticAtom* name : kDefaultHTMLElements) {
UniquePtr<StaticAtomSet> attributes = nullptr;
// Walkthrough the element specific attribute list in lockstep.
// The last "name" in the array is a nullptr sentinel.
if (name == kHTMLElementWithAttributes[i]) {
attributes = MakeUnique<StaticAtomSet>();
while (kHTMLElementWithAttributes[++i]) {
attributes->Insert(kHTMLElementWithAttributes[i]);
}
i++;
}
sDefaultHTMLElements->InsertOrUpdate(name, std::move(attributes));
}
sDefaultMathMLElements =
new ElementsWithAttributes(std::size(kDefaultMathMLElements));
i = 0;
for (nsStaticAtom* name : kDefaultMathMLElements) {
UniquePtr<StaticAtomSet> attributes = nullptr;
if (name == kMathMLElementWithAttributes[i]) {
attributes = MakeUnique<StaticAtomSet>();
while (kMathMLElementWithAttributes[++i]) {
attributes->Insert(kMathMLElementWithAttributes[i]);
}
i++;
}
sDefaultMathMLElements->InsertOrUpdate(name, std::move(attributes));
}
sDefaultAttributes = new StaticAtomSet(std::size(kDefaultAttributes));
for (nsStaticAtom* name : kDefaultAttributes) {
sDefaultAttributes->Insert(name);
}
ClearOnShutdown(&sDefaultHTMLElements);
ClearOnShutdown(&sDefaultMathMLElements);
ClearOnShutdown(&sDefaultAttributes);
}
// https://wicg.github.io/sanitizer-api/#sanitizer-set-a-configuration
void Sanitizer::SetConfig(const SanitizerConfig& aConfig,
bool aAllowCommentsAndDataAttributes,
ErrorResult& aRv) {
// Step 1. For each element of configuration["elements"] do:
if (aConfig.mElements.WasPassed()) {
for (const auto& element : aConfig.mElements.Value()) {
// Step 1.1. Call allow an element with element and sanitizers
// configuration.
AllowElement(element);
}
}
// Step 2. For each element of configuration["removeElements"] do:
if (aConfig.mRemoveElements.WasPassed()) {
for (const auto& element : aConfig.mRemoveElements.Value()) {
// Step 2.1. Call remove an element with element and sanitizers
// configuration.
RemoveElement(element);
}
}
// Step 3. For each element of configuration["replaceWithChildrenElements"]
// do:
if (aConfig.mReplaceWithChildrenElements.WasPassed()) {
for (const auto& element : aConfig.mReplaceWithChildrenElements.Value()) {
// Step 3.1. Call replace an element with its children with element and
// sanitizers configuration.
ReplaceElementWithChildren(element);
}
}
// Step 4. For each attribute of configuration["attributes"] do:
if (aConfig.mAttributes.WasPassed()) {
for (const auto& attribute : aConfig.mAttributes.Value()) {
// Step 4.1. Call allow an attribute with attribute and sanitizers
// configuration.
AllowAttribute(attribute);
}
}
// Step 5. For each attribute of configuration["removeAttributes"] do:
if (aConfig.mRemoveAttributes.WasPassed()) {
for (const auto& attribute : aConfig.mRemoveAttributes.Value()) {
// Step 5.1. Call remove an attribute with attribute and sanitizers
// configuration.
RemoveAttribute(attribute);
}
}
// Step 6. If configuration["comments"] exists:
if (aConfig.mComments.WasPassed()) {
// Step 6.1. Then call set comments with configuration["comments"] and
// sanitizers configuration.
SetComments(aConfig.mComments.Value());
} else {
// Step 6.2. Otherwise call set comments with allowCommentsAndDataAttributes
// and sanitizers configuration.
SetComments(aAllowCommentsAndDataAttributes);
}
// Step 7. If configuration["dataAttributes"] exists:
if (aConfig.mDataAttributes.WasPassed()) {
// Step 7.1. Then call set data attributes with
// configuration["dataAttributes"] and sanitizers configuration.
SetDataAttributes(aConfig.mDataAttributes.Value());
} else {
// Step 7.2. Otherwise call set data attributes with
// allowCommentsAndDataAttributes and sanitizers configuration.
SetDataAttributes(aAllowCommentsAndDataAttributes);
}
// Step 8. Return whether all of the following are true:
auto isSameSize = [](const auto& aInputConfig, const auto& aProcessedConfig) {
size_t sizeInput =
aInputConfig.WasPassed() ? aInputConfig.Value().Length() : 0;
size_t sizeProcessed = aProcessedConfig.Values().Length();
return sizeInput == sizeProcessed;
};
// TODO: Better error messages. (e.g. show difference before and after?)
// size of configuration["elements"] equals size of sanitizers
// configuration["elements"].
if (!isSameSize(aConfig.mElements, mElements)) {
aRv.ThrowTypeError("'elements' changed");
return;
}
// size of configuration["removeElements"] equals size of sanitizers
// configuration["removeElements"].
if (!isSameSize(aConfig.mRemoveElements, mRemoveElements)) {
aRv.ThrowTypeError("'removeElements' changed");
return;
}
// size of configuration["replaceWithChildrenElements"] equals size of
// sanitizers configuration["replaceWithChildrenElements"].
if (!isSameSize(aConfig.mReplaceWithChildrenElements,
mReplaceWithChildrenElements)) {
aRv.ThrowTypeError("'replaceWithChildrenElements' changed");
return;
}
// size of configuration["attributes"] equals size of sanitizers
// configuration["attributes"].
if (!isSameSize(aConfig.mAttributes, mAttributes)) {
aRv.ThrowTypeError("'attributes' changed");
return;
}
// size of configuration["removeAttributes"] equals size of sanitizers
// configuration["removeAttributes"].
if (!isSameSize(aConfig.mRemoveAttributes, mRemoveAttributes)) {
aRv.ThrowTypeError("'removeAttributes' changed");
return;
}
// Either configuration["elements"] or configuration["removeElements"] exist,
// or neither, but not both.
if (aConfig.mElements.WasPassed() && aConfig.mRemoveElements.WasPassed()) {
aRv.ThrowTypeError(
"'elements' and 'removeElements' are not allowed at the same time");
return;
}
// Either configuration["attributes"] or configuration["removeAttributes"]
// exist, or neither, but not both.
if (aConfig.mAttributes.WasPassed() &&
aConfig.mRemoveAttributes.WasPassed()) {
aRv.ThrowTypeError(
"'attributes' and 'removeAttributes' are not allowed at the same time");
return;
}
}
// Turn the lazy default config into real lists that can be
// modified or queried via get().
void Sanitizer::MaybeMaterializeDefaultConfig() {
if (!mIsDefaultConfig) {
return;
}
mIsDefaultConfig = false;
AssertNoLists();
size_t i = 0;
for (nsStaticAtom* name : kDefaultHTMLElements) {
CanonicalElementWithAttributes element(
CanonicalName(name, nsGkAtoms::nsuri_xhtml));
if (name == kHTMLElementWithAttributes[i]) {
ListSet<CanonicalName> attributes;
while (kHTMLElementWithAttributes[++i]) {
attributes.InsertNew(
CanonicalName(kHTMLElementWithAttributes[i], nullptr));
}
i++;
element.mAttributes = Some(std::move(attributes));
}
mElements.InsertNew(std::move(element));
}
i = 0;
for (nsStaticAtom* name : kDefaultMathMLElements) {
CanonicalElementWithAttributes element(
CanonicalName(name, nsGkAtoms::nsuri_mathml));
if (name == kMathMLElementWithAttributes[i]) {
ListSet<CanonicalName> attributes;
while (kMathMLElementWithAttributes[++i]) {
attributes.InsertNew(
CanonicalName(kMathMLElementWithAttributes[i], nullptr));
}
i++;
element.mAttributes = Some(std::move(attributes));
}
mElements.InsertNew(std::move(element));
}
for (nsStaticAtom* name : kDefaultAttributes) {
mAttributes.InsertNew(CanonicalName(name, nullptr));
}
}
void Sanitizer::Get(SanitizerConfig& aConfig) {
MaybeMaterializeDefaultConfig();
nsTArray<OwningStringOrSanitizerElementNamespaceWithAttributes> elements;
for (const CanonicalElementWithAttributes& canonical : mElements.Values()) {
elements.AppendElement()->SetAsSanitizerElementNamespaceWithAttributes() =
canonical.ToSanitizerElementNamespaceWithAttributes();
}
aConfig.mElements.Construct(std::move(elements));
nsTArray<OwningStringOrSanitizerElementNamespace> removeElements;
for (const CanonicalName& canonical : mRemoveElements.Values()) {
removeElements.AppendElement()->SetAsSanitizerElementNamespace() =
canonical.ToSanitizerElementNamespace();
}
aConfig.mRemoveElements.Construct(std::move(removeElements));
nsTArray<OwningStringOrSanitizerElementNamespace> replaceWithChildrenElements;
for (const CanonicalName& canonical : mReplaceWithChildrenElements.Values()) {
replaceWithChildrenElements.AppendElement()
->SetAsSanitizerElementNamespace() =
canonical.ToSanitizerElementNamespace();
}
aConfig.mReplaceWithChildrenElements.Construct(
std::move(replaceWithChildrenElements));
aConfig.mAttributes.Construct(ToSanitizerAttributes(mAttributes));
aConfig.mRemoveAttributes.Construct(ToSanitizerAttributes(mRemoveAttributes));
aConfig.mComments.Construct(mComments);
aConfig.mDataAttributes.Construct(mDataAttributes);
}
auto& GetAsSanitizerElementNamespace(
const StringOrSanitizerElementNamespace& aElement) {
return aElement.GetAsSanitizerElementNamespace();
}
auto& GetAsSanitizerElementNamespace(
const OwningStringOrSanitizerElementNamespace& aElement) {
return aElement.GetAsSanitizerElementNamespace();
}
auto& GetAsSanitizerElementNamespace(
const StringOrSanitizerElementNamespaceWithAttributes& aElement) {
return aElement.GetAsSanitizerElementNamespaceWithAttributes();
}
auto& GetAsSanitizerElementNamespace(
const OwningStringOrSanitizerElementNamespaceWithAttributes& aElement) {
return aElement.GetAsSanitizerElementNamespaceWithAttributes();
}
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element
template <typename SanitizerElement>
static CanonicalName CanonicalizeElement(const SanitizerElement& aElement) {
// return the result of canonicalize a sanitizer name with element and the
// HTML namespace as the default namespace.
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
// Step 1. Assert: name is either a DOMString or a dictionary. (implicit)
// Step 2. If name is a DOMString, then return «[ "name" → name, "namespace" →
// defaultNamespace]».
if (aElement.IsString()) {
RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aElement.GetAsString());
return CanonicalName(nameAtom, nsGkAtoms::nsuri_xhtml);
}
// Step 3. Assert: name is a dictionary and name["name"] exists.
const auto& elem = GetAsSanitizerElementNamespace(aElement);
MOZ_ASSERT(!elem.mName.IsVoid());
RefPtr<nsAtom> namespaceAtom;
// Step 4. Let namespace be name["namespace"] if it exists, otherwise
// defaultNamespace.
//
// Note: "namespace" always exists due to the WebIDL default value.
//
// Step 5. If namespace is the empty string, then set it to null.
// defaultNamespace.
if (!elem.mNamespace.IsEmpty()) {
namespaceAtom = NS_AtomizeMainThread(elem.mNamespace);
}
// Step 6. Return «[
// "name" → name["name"],
// "namespace" → namespace
// )
// ]».
RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(elem.mName);
return CanonicalName(nameAtom, namespaceAtom);
}
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-attribute
template <typename SanitizerAttribute>
static CanonicalName CanonicalizeAttribute(
const SanitizerAttribute& aAttribute) {
// return the result of canonicalize a sanitizer name with attribute and null
// as the default namespace.
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
// Step 1. Assert: name is either a DOMString or a dictionary. (implicit)
// Step 2. If name is a DOMString, then return «[ "name" → name, "namespace" →
// defaultNamespace]».
if (aAttribute.IsString()) {
RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aAttribute.GetAsString());
return CanonicalName(nameAtom, nullptr);
}
// Step 3. Assert: name is a dictionary and name["name"] exists.
const auto& attr = aAttribute.GetAsSanitizerAttributeNamespace();
MOZ_ASSERT(!attr.mName.IsVoid());
RefPtr<nsAtom> namespaceAtom;
// Step 4. Let namespace be name["namespace"] if it exists, otherwise
// defaultNamespace.
// Step 5. If namespace is the empty string, then set it to
// null.
if (!attr.mNamespace.IsEmpty()) {
namespaceAtom = NS_AtomizeMainThread(attr.mNamespace);
}
// Step 6. Return «[
// "name" → name["name"],
// "namespace" → namespace,
// )
// ]».
RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(attr.mName);
return CanonicalName(nameAtom, namespaceAtom);
}
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element-with-attributes
template <typename SanitizerElementWithAttributes>
static CanonicalElementWithAttributes CanonicalizeElementWithAttributes(
const SanitizerElementWithAttributes& aElement) {
// Step 1. Let result be the result of canonicalize a sanitizer element with
// element.
CanonicalElementWithAttributes result =
CanonicalElementWithAttributes(CanonicalizeElement(aElement));
// Step 2. If element is a dictionary:
if (aElement.IsSanitizerElementNamespaceWithAttributes()) {
auto& elem = aElement.GetAsSanitizerElementNamespaceWithAttributes();
// Step 2.1. For each attribute in element["attributes"]:
if (elem.mAttributes.WasPassed()) {
ListSet<CanonicalName> attributes;
for (const auto& attribute : elem.mAttributes.Value()) {
// Step 2.1.1. Add the result of canonicalize a sanitizer attribute with
// attribute to result["attributes"].
attributes.Insert(CanonicalizeAttribute(attribute));
}
result.mAttributes = Some(std::move(attributes));
}
// Step 2.2. For each attribute in element["removeAttributes"]:
if (elem.mRemoveAttributes.WasPassed()) {
ListSet<CanonicalName> attributes;
for (const auto& attribute : elem.mRemoveAttributes.Value()) {
// Step 2.2.1. Add the result of canonicalize a sanitizer attribute with
// attribute to result["removeAttributes"].
attributes.Insert(CanonicalizeAttribute(attribute));
}
result.mRemoveAttributes = Some(std::move(attributes));
}
}
// Step 3. Return result.
return result;
}
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-allow-an-element
template <typename SanitizerElementWithAttributes>
void Sanitizer::AllowElement(const SanitizerElementWithAttributes& aElement) {
MaybeMaterializeDefaultConfig();
// Step 1. Set element to the result of canonicalize a sanitizer element with
// attributes with element.
CanonicalElementWithAttributes element =
CanonicalizeElementWithAttributes(aElement);
// Step 2. Remove element from configuration["elements"].
mElements.Remove(element);
// Step 4. Remove element from configuration["removeElements"].
mRemoveElements.Remove(element);
// Step 5. Remove element from configuration["replaceWithChildrenElements"].
mReplaceWithChildrenElements.Remove(element);
// Step 3. Append element to configuration["elements"].
mElements.Insert(std::move(element));
}
template void Sanitizer::AllowElement(
const StringOrSanitizerElementNamespaceWithAttributes&);
// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-element
template <typename SanitizerElement>
void Sanitizer::RemoveElement(const SanitizerElement& aElement) {
MaybeMaterializeDefaultConfig();
// Step 1. Set element to the result of canonicalize a sanitizer element with
// element.
CanonicalName element = CanonicalizeElement(aElement);
RemoveElementCanonical(std::move(element));
}
void Sanitizer::RemoveElementCanonical(CanonicalName&& aElement) {
// Step 3. Remove element from configuration["elements"] list.
mElements.Remove(aElement);
// Step 4. Remove element from configuration["replaceWithChildrenElements"].
mReplaceWithChildrenElements.Remove(aElement);
// Step 2. Add element to configuration["removeElements"].
mRemoveElements.Insert(std::move(aElement));
}
template void Sanitizer::RemoveElement(
const StringOrSanitizerElementNamespace&);
// https://wicg.github.io/sanitizer-api/#sanitizer-replace-an-element-with-its-children
template <typename SanitizerElement>
void Sanitizer::ReplaceElementWithChildren(const SanitizerElement& aElement) {
MaybeMaterializeDefaultConfig();
// Step 1. Set element to the result of canonicalize a sanitizer element with
// element.
CanonicalName element = CanonicalizeElement(aElement);
// Step 3. Remove element from configuration["removeElements"].
mRemoveElements.Remove(element);
// Step 4. Remove element from configuration["elements"] list.
mElements.Remove(element);
// Step 2. Add element to configuration["replaceWithChildrenElements"].
mReplaceWithChildrenElements.Insert(std::move(element));
}
template void Sanitizer::ReplaceElementWithChildren(
const StringOrSanitizerElementNamespace&);
// https://wicg.github.io/sanitizer-api/#sanitizer-allow-an-attribute
template <typename SanitizerAttribute>
void Sanitizer::AllowAttribute(const SanitizerAttribute& aAttribute) {
MaybeMaterializeDefaultConfig();
// Step 1. Set attribute to the result of canonicalize a sanitizer attribute
// with attribute.
CanonicalName attribute = CanonicalizeAttribute(aAttribute);
// Step 3. Remove attribute from configuration["removeAttributes"].
mRemoveAttributes.Remove(attribute);
// Step 2. Add attribute to configuration["attributes"].
mAttributes.Insert(std::move(attribute));
}
template void Sanitizer::AllowAttribute(
const StringOrSanitizerAttributeNamespace&);
// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-attribute
template <typename SanitizerAttribute>
void Sanitizer::RemoveAttribute(const SanitizerAttribute& aAttribute) {
MaybeMaterializeDefaultConfig();
// Step 1. Set attribute to the result of canonicalize a sanitizer attribute
// with attribute.
CanonicalName attribute = CanonicalizeAttribute(aAttribute);
RemoveAttributeCanonical(std::move(attribute));
}
void Sanitizer::RemoveAttributeCanonical(CanonicalName&& aAttribute) {
// Step 3. Remove attribute from configuration["attributes"].
mAttributes.Remove(aAttribute);
// Step 2. Add attribute to configuration["removeAttributes"].
mRemoveAttributes.Insert(std::move(aAttribute));
}
template void Sanitizer::RemoveAttribute(
const StringOrSanitizerAttributeNamespace&);
void Sanitizer::SetComments(bool aAllow) {
// The sanitize algorithm optimized for the default config supports
// comments both being allowed and disallowed.
mComments = aAllow;
}
void Sanitizer::SetDataAttributes(bool aAllow) {
// Same as above for data-attributes.
mDataAttributes = aAllow;
}
// https://wicg.github.io/sanitizer-api/#sanitizer-removeunsafe
void Sanitizer::RemoveUnsafe() {
MaybeMaterializeDefaultConfig();
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
// Step 1. Assert: (Implicit)
// Step 2. Let result be a copy of configuration. (Unobservable)
// Step 3. For each element in built-in safe baseline
// configuration[removeElements]:
//
// Keep in sync with IsUnsafeElement
RemoveElementCanonical(
CanonicalName(nsGkAtoms::script, nsGkAtoms::nsuri_xhtml));
RemoveElementCanonical(
CanonicalName(nsGkAtoms::frame, nsGkAtoms::nsuri_xhtml));
RemoveElementCanonical(
CanonicalName(nsGkAtoms::iframe, nsGkAtoms::nsuri_xhtml));
RemoveElementCanonical(
CanonicalName(nsGkAtoms::object, nsGkAtoms::nsuri_xhtml));
RemoveElementCanonical(
CanonicalName(nsGkAtoms::embed, nsGkAtoms::nsuri_xhtml));
RemoveElementCanonical(
CanonicalName(nsGkAtoms::script, nsGkAtoms::nsuri_svg));
RemoveElementCanonical(CanonicalName(nsGkAtoms::use, nsGkAtoms::nsuri_svg));
// Step 4. For each attribute in built-in safe baseline
// configuration[removeAttributes]: (Empty list)
// Step 5. For each attribute listed in event handler content attributes:
// TODO: Consider sorting these.
nsContentUtils::ForEachEventAttributeName(
EventNameType_All & ~EventNameType_XUL,
[self = MOZ_KnownLive(this)](nsAtom* aName) {
self->RemoveAttributeCanonical(CanonicalName(aName, nullptr));
});
// Step 6. Return result. (Overwrites "thiss configuration")
}
// https://wicg.github.io/sanitizer-api/#sanitize
void Sanitizer::Sanitize(nsINode* aNode, bool aSafe, ErrorResult& aRv) {
MOZ_ASSERT(aNode->OwnerDoc()->IsLoadedAsData(),
"SanitizeChildren relies on the document being inert to be safe");
// Step 1. Let configuration be the value of sanitizers configuration.
// Step 2. If safe is true, then set configuration to the result of calling
// remove unsafe on configuration.
//
// Optimization: We really don't want to make a copy of the configuration
// here, so we instead explictly remove the handful elements and
// attributes that are part of "remove unsafe" in the
// SanitizeChildren() and SanitizeAttributes() methods.
// Step 3. Call sanitize core on node, configuration, and with
// handleJavascriptNavigationUrls set to safe.
if (mIsDefaultConfig) {
AssertNoLists();
SanitizeChildren<true>(aNode, aSafe);
} else {
SanitizeChildren<false>(aNode, aSafe);
}
}
static RefPtr<nsAtom> ToNamespace(int32_t aNamespaceID) {
if (aNamespaceID == kNameSpaceID_None) {
return nullptr;
}
RefPtr<nsAtom> atom =
nsNameSpaceManager::GetInstance()->NameSpaceURIAtom(aNamespaceID);
return atom;
}
// https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
// The "removeElements" list.
// Keep in sync with Sanitizer::RemoveUnsafe
static bool IsUnsafeElement(nsAtom* aLocalName, int32_t aNamespaceID) {
if (aNamespaceID == kNameSpaceID_XHTML) {
return aLocalName == nsGkAtoms::script || aLocalName == nsGkAtoms::frame ||
aLocalName == nsGkAtoms::iframe || aLocalName == nsGkAtoms::object ||
aLocalName == nsGkAtoms::embed;
}
if (aNamespaceID == kNameSpaceID_SVG) {
return aLocalName == nsGkAtoms::script || aLocalName == nsGkAtoms::use;
}
return false;
}
// https://wicg.github.io/sanitizer-api/#sanitize-core
template <bool IsDefaultConfig>
void Sanitizer::SanitizeChildren(nsINode* aNode, bool aSafe) {
// Step 1. For each child in currents children:
nsCOMPtr<nsIContent> next = nullptr;
for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild(); child;
child = next) {
next = child->GetNextSibling();
// Step 1.1. Assert: child implements Text, Comment, Element, or
// DocumentType.
MOZ_ASSERT(child->IsText() || child->IsComment() || child->IsElement() ||
child->NodeType() == nsINode::DOCUMENT_TYPE_NODE);
// Step 1.2. If child implements DocumentType, then continue.
if (child->NodeType() == nsINode::DOCUMENT_TYPE_NODE) {
continue;
}
// Step 1.3. If child implements Text, then continue.
if (child->IsText()) {
continue;
}
// Step 1.4. If child implements Comment:
if (child->IsComment()) {
// Step 1.4.1 If configuration["comments"] is not true, then remove child.
if (!mComments) {
child->RemoveFromParent();
}
continue;
}
// Step 1.5. Otherwise:
MOZ_ASSERT(child->IsElement());
// Step 1.5.1. Let elementName be a SanitizerElementNamespace with childs
// local name and namespace.
nsAtom* nameAtom = child->NodeInfo()->NameAtom();
int32_t namespaceID = child->NodeInfo()->NamespaceID();
// Make sure this is optimized away for the default config.
Maybe<CanonicalName> elementName;
if constexpr (!IsDefaultConfig) {
elementName.emplace(nameAtom, ToNamespace(namespaceID));
}
// Optimization: Remove unsafe elements before doing anything else.
// https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
//
// We have to do this _before_ handling the "replaceWithChildrenElements"
// list, because by adding the unsafe elements to the "removeElements" list
// they would be implicitly deleted from the former.
//
// The default config's "elements" allow list does not contain any unsafe
// elements so we can skip this.
if constexpr (!IsDefaultConfig) {
if (aSafe && IsUnsafeElement(nameAtom, namespaceID)) {
child->RemoveFromParent();
continue;
}
}
// Step 1.5.2. If configuration["replaceWithChildrenElements"] contains
// elementName:
if constexpr (!IsDefaultConfig) {
if (mReplaceWithChildrenElements.Contains(*elementName)) {
// Note: This follows nsTreeSanitizer by first inserting the
// child's children in place of the current child and then
// continueing the sanitization from the first inserted grandchild.
nsCOMPtr<nsIContent> parent = child->GetParent();
nsCOMPtr<nsIContent> firstChild = child->GetFirstChild();
nsCOMPtr<nsIContent> newChild = firstChild;
for (; newChild; newChild = child->GetFirstChild()) {
ErrorResult rv;
parent->InsertBefore(*newChild, child, rv);
if (rv.Failed()) {
// TODO: Abort?
break;
}
}
child->RemoveFromParent();
if (firstChild) {
next = firstChild;
}
continue;
}
}
// Step 1.5.3. If configuration["removeElements"] contains elementName, or
// if configuration["elements"] is not empty and does not contain
// elementName:
[[maybe_unused]] StaticAtomSet* elementAttributes = nullptr;
if constexpr (!IsDefaultConfig) {
if (mRemoveElements.Contains(*elementName) ||
(!mElements.IsEmpty() && !mElements.Contains(*elementName))) {
// Step 1.5.3.1. Remove child.
child->RemoveFromParent();
// Step 1.5.3.2. Continue.
continue;
}
} else {
bool found = false;
if (nameAtom->IsStatic()) {
ElementsWithAttributes* elements = nullptr;
if (namespaceID == kNameSpaceID_XHTML) {
elements = sDefaultHTMLElements;
} else if (namespaceID == kNameSpaceID_MathML) {
elements = sDefaultMathMLElements;
}
if (elements) {
if (auto lookup = elements->Lookup(nameAtom->AsStatic())) {
found = true;
// This is the nullptr for elements without specific allowed
// attributes.
elementAttributes = lookup->get();
}
}
}
if (!found) {
// Step 1.5.3.1. Remove child.
child->RemoveFromParent();
// Step 1.5.3.2. Continue.
continue;
}
MOZ_ASSERT(!IsUnsafeElement(nameAtom, namespaceID));
}
// Step 1.5.4. If elementName equals «[ "name" → "template", "namespace" →
// HTML namespace ]»
if (auto* templateEl = HTMLTemplateElement::FromNode(child)) {
// Step 1.5.4.1. Then call sanitize core on childs template contents with
// configuration and handleJavascriptNavigationUrls.
RefPtr<DocumentFragment> frag = templateEl->Content();
SanitizeChildren<IsDefaultConfig>(frag, aSafe);
}
// Step 1.5.5. If child is a shadow host, then call sanitize core on childs
// shadow root with configuration and handleJavascriptNavigationUrls.
if (RefPtr<ShadowRoot> shadow = child->GetShadowRoot()) {
SanitizeChildren<IsDefaultConfig>(shadow, aSafe);
}
// Step 1.5.6.
if constexpr (!IsDefaultConfig) {
SanitizeAttributes(child->AsElement(), *elementName, aSafe);
} else {
SanitizeDefaultConfigAttributes(child->AsElement(), elementAttributes,
aSafe);
}
// Step 1.5.7. Call sanitize core on child with configuration and
// handleJavascriptNavigationUrls.
// TODO: Optimization: Remove recusion similar to nsTreeSanitizer
SanitizeChildren<IsDefaultConfig>(child, aSafe);
}
}
static inline bool IsDataAttribute(nsAtom* aName, int32_t aNamespaceID) {
return StringBeginsWith(nsDependentAtomString(aName), u"data-"_ns) &&
aNamespaceID == kNameSpaceID_None;
}
// https://wicg.github.io/sanitizer-api/#sanitize-core
// Step 2.4.6.5. If handleJavascriptNavigationUrls:
static bool RemoveJavascriptNavigationURLAttribute(Element* aElement,
nsAtom* aLocalName,
int32_t aNamespaceID) {
auto containsJavascriptURL = [&]() {
nsAutoString value;
if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
return false;
}
// https://wicg.github.io/sanitizer-api/#contains-a-javascript-url
// Step 1. Let url be the result of running the basic URL parser on
// attributes value.
// XXX follow base-uri?
nsCOMPtr<nsIURI> uri;
if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), value))) {
// Step 2. If url is failure, then return false.
return false;
}
// Step 3. Return whether urls scheme is "javascript".
return uri->SchemeIs("javascript");
};
// Step 1. If «[elementName, attrName]» matches an entry in the built-in
// navigating URL attributes list, and if attribute contains a javascript:
// URL, then remove attribute from child.
if ((aElement->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
nsGkAtoms::base) &&
aLocalName == nsGkAtoms::href && aNamespaceID == kNameSpaceID_None) ||
(aElement->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input) &&
aLocalName == nsGkAtoms::formaction &&
aNamespaceID == kNameSpaceID_None) ||
(aElement->IsHTMLElement(nsGkAtoms::form) &&
aLocalName == nsGkAtoms::action && aNamespaceID == kNameSpaceID_None) ||
(aElement->IsHTMLElement(nsGkAtoms::iframe) &&
aLocalName == nsGkAtoms::src && aNamespaceID == kNameSpaceID_None) ||
(aElement->IsSVGElement(nsGkAtoms::a) && aLocalName == nsGkAtoms::href &&
(aNamespaceID == kNameSpaceID_None ||
aNamespaceID == kNameSpaceID_XLink))) {
if (containsJavascriptURL()) {
return true;
}
};
// Step 2. If childs namespace is the MathML Namespace and attrs local name
// is "href" and attrs namespace is null or the XLink namespace and attr
// contains a javascript: URL, then remove attr.
if (aElement->IsMathMLElement() && aLocalName == nsGkAtoms::href &&
(aNamespaceID == kNameSpaceID_None ||
aNamespaceID == kNameSpaceID_XLink)) {
if (containsJavascriptURL()) {
return true;
}
}
// Step 3. If the built-in animating URL attributes list contains
// «[elementName, attrName]» and attrs value is "href" or "xlink:href", then
// remove attr.
if (aLocalName == nsGkAtoms::attributeName &&
aNamespaceID == kNameSpaceID_None &&
aElement->IsAnyOfSVGElements(nsGkAtoms::animate, nsGkAtoms::animateMotion,
nsGkAtoms::animateTransform,
nsGkAtoms::set)) {
nsAutoString value;
if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
return false;
}
return value.EqualsLiteral("href") || value.EqualsLiteral("xlink:href");
}
return false;
}
void Sanitizer::SanitizeAttributes(Element* aChild,
const CanonicalName& aElementName,
bool aSafe) {
MOZ_ASSERT(!mIsDefaultConfig);
// TODO: Replace this with a hashmap.
const CanonicalElementWithAttributes* elementWithAttributes =
mElements.Get(aElementName);
// https://wicg.github.io/sanitizer-api/#sanitize-core
// Substeps of
// Step 1.5.6. For each attribute in childs attribute list:
int32_t count = int32_t(aChild->GetAttrCount());
for (int32_t i = count - 1; i >= 0; --i) {
// Step 1. Let attrName be a SanitizerAttributeNamespace with attributes
// local name and namespace.
const nsAttrName* attr = aChild->GetAttrNameAt(i);
RefPtr<nsAtom> attrLocalName = attr->LocalName();
int32_t attrNs = attr->NamespaceID();
CanonicalName attrName(attrLocalName, ToNamespace(attrNs));
bool remove = false;
// Optimization: Remove unsafe event handler content attributes.
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
if (aSafe && attrNs == kNameSpaceID_None &&
nsContentUtils::IsEventAttributeName(
attrLocalName, EventNameType_All & ~EventNameType_XUL)) {
remove = true;
}
// Step 2. If configuration["removeAttributes"] contains attrName, then
// Remove attribute from child.
else if (mRemoveAttributes.Contains(attrName)) {
remove = true;
}
// Step 3. If configuration["elements"]["removeAttributes"] contains
// attrName, then remove attribute from child.
// XXX:
// Spec issue configuration["elements"][elementName]["removeAttributes"] ??
else if (elementWithAttributes &&
elementWithAttributes->mRemoveAttributes &&
elementWithAttributes->mRemoveAttributes->Contains(attrName)) {
remove = true;
}
// Step 4. If all of the following are false, then remove attribute from
// child.
// - configuration["attributes"] exists and contains attrName
// TODO: exists check
// - configuration["elements"]["attributes"] contains attrName
// - "data-" is a code unit prefix of local name and namespace is null and
// configuration["dataAttributes"] is true
else if ((!mAttributes.IsEmpty() && !mAttributes.Contains(attrName)) &&
!(elementWithAttributes && elementWithAttributes->mAttributes &&
elementWithAttributes->mAttributes->Contains(attrName)) &&
!(mDataAttributes && IsDataAttribute(attrLocalName, attrNs))) {
remove = true;
}
// Step 5. If handleJavascriptNavigationUrls:
else if (aSafe) {
remove =
RemoveJavascriptNavigationURLAttribute(aChild, attrLocalName, attrNs);
}
if (remove) {
aChild->UnsetAttr(attr->NamespaceID(), attr->LocalName(), false);
// XXX Copied from nsTreeSanitizer.
// In case the attribute removal shuffled the attribute order, start the
// loop again.
--count;
i = count; // i will be decremented immediately thanks to the for loop
}
}
}
void Sanitizer::SanitizeDefaultConfigAttributes(
Element* aChild, StaticAtomSet* aElementAttributes, bool aSafe) {
MOZ_ASSERT(mIsDefaultConfig);
// https://wicg.github.io/sanitizer-api/#sanitize-core
// Substeps of
// Step 1.5.6. For each attribute in childs attribute list:
int32_t count = int32_t(aChild->GetAttrCount());
for (int32_t i = count - 1; i >= 0; --i) {
// Step 1. Let attrName be a SanitizerAttributeNamespace with attributes
// local name and namespace.
const nsAttrName* attr = aChild->GetAttrNameAt(i);
RefPtr<nsAtom> attrLocalName = attr->LocalName();
int32_t attrNs = attr->NamespaceID();
// Step 2. If configuration["removeAttributes"] contains attrName, then
// Remove attribute from child.
// Step 3. If configuration["elements"]["removeAttributes"] contains
// attrName, then remove attribute from child.
//
// Note: Empty/missing for the default config.
// Step 4. If all of the following are false, then remove attribute from
// child.
// - configuration["attributes"] exists and contains attrName
// - configuration["elements"]["attributes"] contains attrName
// - "data-" is a code unit prefix of local name and namespace is null and
// configuration["dataAttributes"] is true
bool remove = false;
// Note: All attributes allowed by the default config are in the "null"
// namespace.
if (attrNs != kNameSpaceID_None ||
(!sDefaultAttributes->Contains(attrLocalName) &&
!(aElementAttributes && aElementAttributes->Contains(attrLocalName)) &&
!(mDataAttributes && IsDataAttribute(attrLocalName, attrNs)))) {
remove = true;
}
// Step 5. If handleJavascriptNavigationUrls:
else if (aSafe) {
// TODO: This can be further optimized, because the default config
// at the moment only allows <a href>.
remove =
RemoveJavascriptNavigationURLAttribute(aChild, attrLocalName, attrNs);
}
// The default config attribute allow lists don't contain event
// handler attributes.
MOZ_ASSERT_IF(!remove,
!nsContentUtils::IsEventAttributeName(
attrLocalName, EventNameType_All & ~EventNameType_XUL));
if (remove) {
aChild->UnsetAttr(attr->NamespaceID(), attr->LocalName(), false);
// XXX Copied from nsTreeSanitizer.
// In case the attribute removal shuffled the attribute order, start the
// loop again.
--count;
i = count; // i will be decremented immediately thanks to the for loop
}
}
}
/* ------ Logging ------ */
void Sanitizer::LogLocalizedString(const char* aName,
const nsTArray<nsString>& aParams,
uint32_t aFlags) {
uint64_t innerWindowID = 0;
bool isPrivateBrowsing = true;
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
if (window && window->GetDoc()) {
auto* doc = window->GetDoc();
innerWindowID = doc->InnerWindowID();
isPrivateBrowsing = doc->IsInPrivateBrowsing();
}
nsAutoString logMsg;
nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
aName, aParams, logMsg);
LogMessage(logMsg, aFlags, innerWindowID, isPrivateBrowsing);
}
/* static */
void Sanitizer::LogMessage(const nsAString& aMessage, uint32_t aFlags,
uint64_t aInnerWindowID, bool aFromPrivateWindow) {
// Prepending 'Sanitizer' to the outgoing console message
nsString message;
message.AppendLiteral(u"Sanitizer: ");
message.Append(aMessage);
// Allow for easy distinction in devtools code.
constexpr auto category = "Sanitizer"_ns;
if (aInnerWindowID > 0) {
// Send to content console
nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category,
aInnerWindowID);
} else {
// Send to browser console
nsContentUtils::LogSimpleConsoleError(message, category, aFromPrivateWindow,
true /* from chrome context */,
aFlags);
}
}
} // namespace mozilla::dom