blob: 3dcf7e30cb190c19da6578730cb5860cce631b8d [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "sanitizer.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_node_filter.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_parse_from_string_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_document_documentfragment.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_sanitizer_config.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/dom/document_init.h"
#include "third_party/blink/renderer/core/dom/dom_implementation.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/html/custom/custom_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/html/html_area_element.h"
#include "third_party/blink/renderer/core/html/html_collection.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_template_element.h"
#include "third_party/blink/renderer/core/xml/dom_parser.h"
#include "third_party/blink/renderer/modules/sanitizer_api/builtins.h"
#include "third_party/blink/renderer/modules/sanitizer_api/config_util.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
namespace blink {
namespace {
bool ConfigIsEmpty(const SanitizerConfig* config) {
return !config ||
(!config->hasDropElements() && !config->hasBlockElements() &&
!config->hasAllowElements() && !config->hasDropAttributes() &&
!config->hasAllowAttributes() && !config->hasAllowCustomElements() &&
!config->hasAllowComments() && !config->hasAllowUnknownMarkup());
}
String FromAPI(Node* node) {
DCHECK(node);
DCHECK(node->IsElementNode());
Element* element = To<Element>(node);
if (element->IsSVGElement())
return "svg:" + element->localName();
if (node->IsMathMLElement())
return "math:" + element->localName();
return element->localName();
}
} // anonymous namespace
Sanitizer::Sanitizer(ExecutionContext* execution_context,
const SanitizerConfig* config) {
// The spec treats an absent config as the default. We'll handle this by
// checking for this an empty dictionary and representing it as nullptr.
if (ConfigIsEmpty(config)) {
config = nullptr;
UseCounter::Count(execution_context,
WebFeature::kSanitizerAPIDefaultConfiguration);
}
config_ = FromAPI(config);
}
Sanitizer::~Sanitizer() = default;
Sanitizer* Sanitizer::Create(ExecutionContext* execution_context,
const SanitizerConfig* config,
ExceptionState& exception_state) {
return MakeGarbageCollected<Sanitizer>(execution_context, config);
}
DocumentFragment* Sanitizer::sanitize(ScriptState* script_state,
const V8SanitizerInput* input,
ExceptionState& exception_state) {
V8SanitizerInput* new_input = nullptr;
switch (input->GetContentType()) {
case V8SanitizerInput::ContentType::kDocument:
new_input =
MakeGarbageCollected<V8SanitizerInput>(input->GetAsDocument());
break;
case V8SanitizerInput::ContentType::kDocumentFragment:
new_input = MakeGarbageCollected<V8SanitizerInput>(
input->GetAsDocumentFragment());
break;
}
LocalDOMWindow* window = LocalDOMWindow::From(script_state);
DocumentFragment* fragment =
PrepareFragment(window, script_state, new_input, exception_state);
if (exception_state.HadException()) {
return nullptr;
}
DoSanitizing(fragment, window, exception_state);
return fragment;
}
Element* Sanitizer::sanitizeFor(ScriptState* script_state,
const String& local_name,
const String& markup,
ExceptionState& exception_state) {
LocalDOMWindow* window = LocalDOMWindow::From(script_state);
Document* inert_document = DocumentInit::Create()
.WithURL(window->Url())
.WithTypeFrom("text/html")
.WithExecutionContext(window)
.WithAgent(*window->GetAgent())
.CreateDocument();
Element* element = inert_document->CreateElementForBinding(
AtomicString(local_name), exception_state);
if (exception_state.HadException()) {
exception_state.ClearException();
return nullptr;
}
// 2. If the element kind [...] is regular and if the baseline
// element allow list does not contain element name, then return null.
// See: https://wicg.github.io/sanitizer-api/#sanitizefor
bool is_regular =
element->IsHTMLElement() &&
!To<HTMLElement>(element)->IsHTMLUnknownElement() &&
!CustomElement::IsValidName(AtomicString(local_name), false);
if (is_regular && !Match(FromAPI(element), GetBaselineAllowElements())) {
return nullptr;
}
element->setInnerHTML(markup, exception_state);
if (exception_state.HadException()) {
exception_state.ClearException();
return nullptr;
}
DoSanitizing(element, window, exception_state);
if (exception_state.HadException()) {
exception_state.ClearException();
return nullptr;
}
// Edge case: The template element treatment also applies to the newly created
// element in .sanitizeFor.
if (IsA<HTMLTemplateElement>(element)) {
DoSanitizing(To<HTMLTemplateElement>(element)->content(), window,
exception_state);
if (exception_state.HadException()) {
exception_state.ClearException();
return nullptr;
}
}
return element;
}
void Sanitizer::ElementSetHTML(ScriptState* script_state,
Element& element,
const String& markup,
ExceptionState& exception_state) {
Element* new_element =
sanitizeFor(script_state, element.localName(), markup, exception_state);
if (exception_state.HadException())
return;
if (!new_element) {
exception_state.ThrowTypeError("setHTML not allowed on this element type.");
return;
}
element.RemoveChildren();
while (Node* to_be_moved = new_element->firstChild())
element.AppendChild(to_be_moved);
}
DocumentFragment* Sanitizer::PrepareFragment(LocalDOMWindow* window,
ScriptState* script_state,
const V8SanitizerInput* input,
ExceptionState& exception_state) {
DCHECK(input);
if (!window) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Cannot find current DOM window.");
return nullptr;
}
switch (input->GetContentType()) {
case V8SanitizerInput::ContentType::kDocument: {
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIFromDocument);
DocumentFragment* fragment =
input->GetAsDocument()->createDocumentFragment();
fragment->CloneChildNodesFrom(*(input->GetAsDocument()->body()),
CloneChildrenFlag::kClone);
return fragment;
}
case V8SanitizerInput::ContentType::kDocumentFragment:
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIFromFragment);
return input->GetAsDocumentFragment();
}
NOTREACHED();
return nullptr;
}
void Sanitizer::DoSanitizing(ContainerNode* fragment,
LocalDOMWindow* window,
ExceptionState& exception_state) {
Node* node = fragment->firstChild();
while (node) {
switch (node->getNodeType()) {
case Node::NodeType::kElementNode: {
Element* element = To<Element>(node);
// 1. Let |name| be |element|'s tag name.
String name = FromAPI(element);
// 2. Detect element kind. (regular element, custom element, or else.)
bool is_custom_element =
CustomElement::IsValidName(AtomicString(name), false);
bool is_unknown_element =
element->IsHTMLElement() &&
To<HTMLElement>(element)->IsHTMLUnknownElement();
bool is_regular = !is_custom_element && !is_unknown_element;
// 3. If |kind| is `regular` and if |name| is not contained in the
// baseline element allow list, then 'drop'
if (is_regular && !Match(name, GetBaselineAllowElements())) {
node = DropNode(element, fragment);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (is_custom_element && !config_.allow_custom_elements_) {
// 4. If |kind| is `custom` and if allow_custom_elements_ is unset or
// set to anything other than `true`, then 'drop'.
node = DropNode(element, fragment);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (is_unknown_element && !config_.allow_unknown_markup_) {
// TODO: No spec yet. https://github.com/WICG/sanitizer-api/pull/159
node = DropNode(element, fragment);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (Match(name, config_.drop_elements_)) {
// 5. If |name| is in |config|'s [=element drop list=] then 'drop'.
node = DropNode(element, fragment);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (Match(name, config_.block_elements_)) {
// 6. If |name| is in |config|'s [=element block list=] then 'block'.
node = BlockElement(element, fragment, exception_state);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (!Match(name, config_.allow_elements_)) {
// 7. if |name| is not in |config|'s [=element allow list=] then
// 'block'.
node = BlockElement(element, fragment, exception_state);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
} else if (IsA<HTMLTemplateElement>(element)) {
// 8. If |element|'s [=element interface=] is {{HTMLTemplateElement}}
// Run the steps of the [=sanitize document fragment=] algorithm on
// |element|'s |content| attribute.
DoSanitizing(To<HTMLTemplateElement>(element)->content(), window,
exception_state);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
node = KeepElement(element, fragment, window);
} else {
node = KeepElement(element, fragment, window);
}
break;
}
case Node::NodeType::kTextNode:
// Text node: Keep (by skipping over it).
node = NodeTraversal::Next(*node, fragment);
break;
case Node::NodeType::kCommentNode:
// Comment: Drop (unless allowed by config).
node = config_.allow_comments_ ? NodeTraversal::Next(*node, fragment)
: DropNode(node, fragment);
break;
case Node::NodeType::kDocumentNode:
case Node::NodeType::kDocumentFragmentNode:
// Document & DocumentFragment: Drop (unless it's the root).
node = !node->parentNode() ? NodeTraversal::Next(*node, fragment)
: DropNode(node, fragment);
break;
default:
// Default: Drop anything not explicitly handled.
node = DropNode(node, fragment);
break;
}
}
}
// If the current node needs to be dropped, remove current node entirely
// and proceed to its next sibling.
Node* Sanitizer::DropNode(Node* node, ContainerNode* fragment) {
Node* tmp = node;
node = NodeTraversal::NextSkippingChildren(*node, fragment);
tmp->remove();
return node;
}
// If the current element should be blocked, append its children after current
// node to parent node, remove current element and proceed to the next node.
Node* Sanitizer::BlockElement(Element* element,
ContainerNode* fragment,
ExceptionState& exception_state) {
ContainerNode* parent = element->parentNode();
Node* next_sibling = element->nextSibling();
while (Node* child = element->firstChild()) {
parent->InsertBefore(child, next_sibling, exception_state);
if (exception_state.HadException()) {
return nullptr;
}
}
Node* tmp = element;
Node* result = NodeTraversal::Next(*element, fragment);
tmp->remove();
return result;
}
// Remove any attributes to be dropped from the current element, and proceed to
// the next node (preorder, depth-first traversal).
Node* Sanitizer::KeepElement(Element* element,
ContainerNode* fragment,
LocalDOMWindow* window) {
String node_name = FromAPI(element);
if (Match(Wildcard(), node_name, config_.drop_attributes_)) {
for (const auto& name : element->getAttributeNames()) {
element->removeAttribute(name);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
}
} else {
bool allow_attributes_wildcard =
Match(Wildcard(), node_name, config_.allow_attributes_);
for (const auto& name : element->getAttributeNames()) {
// Attributes in drop list or not in allow list while allow list
// exists will be dropped.
bool is_unknown = !Match(name, node_name, GetKnownAttributes());
bool drop = (is_unknown ? !config_.allow_unknown_markup_
: !Match(name, node_name,
GetBaselineAllowAttributes())) ||
Match(name, node_name, config_.drop_attributes_) ||
(!allow_attributes_wildcard &&
!Match(name, node_name, config_.allow_attributes_));
// 9. If |element|'s [=element interface=] is {{HTMLAnchorElement}} or
// {{HTMLAreaElement}} and |element|'s `protocol` property is
// "javascript:", then remove the `href` attribute from |element|.
if (IsA<HTMLAnchorElement>(element) && name == "href" &&
To<HTMLAnchorElement>(element)->Href().Protocol() == "javascript") {
drop = true;
} else if (IsA<HTMLAreaElement>(element) && name == "href" &&
To<HTMLAreaElement>(element)->Href().Protocol() ==
"javascript") {
drop = true;
} else if (IsA<HTMLFormElement>(element) && name == "action" &&
To<HTMLFormElement>(element)->action().StartsWith(
"javascript:")) {
// 10. If |element|'s [=element interface=] is {{HTMLFormElement}} and
// |element|'s `action` attribute is a [[URL]] with `javascript:`
// protocol, them drop it.
drop = true;
} else if ((IsA<HTMLInputElement>(element) ||
IsA<HTMLButtonElement>(element)) &&
name == "formaction" &&
To<HTMLFormControlElement>(element)->formAction().StartsWith(
"javascript:")) {
// 11. If |element|'s [=element interface=] is {{HTMLInputElement}}
// or {{HTMLButtonElement}} and |element|'s `action` attribute is a
// [[URL]] with `javascript:` protocol, them drop it.
drop = true;
}
if (drop) {
element->removeAttribute(name);
UseCounter::Count(window->GetExecutionContext(),
WebFeature::kSanitizerAPIActionTaken);
}
}
}
return NodeTraversal::Next(*element, fragment);
}
SanitizerConfig* Sanitizer::getConfiguration() const {
return ToAPI(config_);
}
SanitizerConfig* Sanitizer::getDefaultConfiguration() {
return ToAPI(GetDefaultConfig());
}
Sanitizer* Sanitizer::getDefaultInstance() {
DEFINE_STATIC_LOCAL(
Persistent<Sanitizer>, default_sanitizer_,
(Sanitizer::Create(nullptr, nullptr, ASSERT_NO_EXCEPTION)));
return default_sanitizer_;
}
void Sanitizer::Trace(Visitor* visitor) const {
ScriptWrappable::Trace(visitor);
}
} // namespace blink