| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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/web_feature/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/modules/v8/string_or_document_fragment_or_document.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/string_or_trusted_html_or_document_fragment_or_document.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/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/trustedtypes/trusted_html.h" |
| #include "third_party/blink/renderer/core/trustedtypes/trusted_types_util.h" |
| #include "third_party/blink/renderer/core/xml/dom_parser.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" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| |
| namespace blink { |
| |
| Sanitizer* Sanitizer::Create(ExecutionContext* execution_context, |
| const SanitizerConfig* config, |
| ExceptionState& exception_state) { |
| return MakeGarbageCollected<Sanitizer>(execution_context, config); |
| } |
| |
| Sanitizer::Sanitizer(ExecutionContext* execution_context, |
| const SanitizerConfig* config) |
| : allow_custom_elements_(config->allowCustomElements()) { |
| bool use_default_config = true; |
| if (config->allowCustomElements()) { |
| use_default_config = false; |
| } |
| |
| // Format dropElements to uppercase. |
| drop_elements_ = default_drop_elements_; |
| if (config->hasDropElements()) { |
| ElementFormatter(drop_elements_, config->dropElements()); |
| use_default_config = false; |
| } |
| |
| // Format blockElements to uppercase. |
| block_elements_ = default_block_elements_; |
| if (config->hasBlockElements()) { |
| ElementFormatter(block_elements_, config->blockElements()); |
| use_default_config = false; |
| } |
| |
| // Format allowElements to uppercase. |
| if (config->hasAllowElements()) { |
| has_allow_elements_ = true; |
| ElementFormatter(allow_elements_, config->allowElements()); |
| use_default_config = false; |
| } |
| |
| // Format dropAttributes to lowercase. |
| drop_attributes_ = default_drop_attributes_; |
| if (config->hasDropAttributes()) { |
| AttrFormatter(drop_attributes_, config->dropAttributes()); |
| use_default_config = false; |
| } |
| |
| // Format allowAttributes to lowercase. |
| if (config->hasAllowAttributes()) { |
| has_allow_attributes_ = true; |
| AttrFormatter(allow_attributes_, config->allowAttributes()); |
| use_default_config = false; |
| } |
| |
| if (use_default_config) { |
| // TODO(lyf): Add unit tests for counters. |
| UseCounter::Count(execution_context, |
| WebFeature::kSanitizerAPIDefaultConfiguration); |
| } |
| } |
| |
| void Sanitizer::ElementFormatter(HashSet<String>& element_set, |
| const Vector<String>& elements) { |
| for (const String& s : elements) { |
| element_set.insert(s.UpperASCII()); |
| } |
| } |
| |
| void Sanitizer::AttrFormatter( |
| HashMap<String, Vector<String>>& attr_map, |
| const Vector<std::pair<String, Vector<String>>>& attrs) { |
| for (const std::pair<String, Vector<String>>& pair : attrs) { |
| const String& lower_attr = pair.first.LowerASCII(); |
| if (pair.second == kVectorStar || pair.second.Contains("*")) { |
| attr_map.insert(lower_attr, kVectorStar); |
| } else { |
| Vector<String> elements; |
| for (const String& s : pair.second) { |
| elements.push_back(s.UpperASCII()); |
| } |
| attr_map.insert(lower_attr, elements); |
| } |
| } |
| } |
| |
| Sanitizer::~Sanitizer() = default; |
| |
| String Sanitizer::sanitizeToString(ScriptState* script_state, |
| StringOrDocumentFragmentOrDocument& input, |
| ExceptionState& exception_state) { |
| return CreateMarkup(SanitizeImpl(script_state, input, exception_state), |
| kChildrenOnly); |
| } |
| |
| DocumentFragment* Sanitizer::sanitize( |
| ScriptState* script_state, |
| StringOrTrustedHTMLOrDocumentFragmentOrDocument& input, |
| ExceptionState& exception_state) { |
| StringOrDocumentFragmentOrDocument new_input; |
| if (input.IsString() || input.IsNull()) { |
| LocalDOMWindow* window = LocalDOMWindow::From(script_state); |
| if (!window) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Cannot find current DOM window."); |
| return nullptr; |
| } |
| new_input.SetString(TrustedTypesCheckForHTML( |
| input.GetAsString(), window->GetExecutionContext(), exception_state)); |
| if (exception_state.HadException()) { |
| return nullptr; |
| } |
| } else if (input.IsTrustedHTML()) { |
| new_input.SetString(input.GetAsTrustedHTML()->toString()); |
| } else if (input.IsDocument()) { |
| new_input.SetDocument(input.GetAsDocument()); |
| } else if (input.IsDocumentFragment()) { |
| new_input.SetDocumentFragment(input.GetAsDocumentFragment()); |
| } |
| return SanitizeImpl(script_state, new_input, exception_state); |
| } |
| |
| DocumentFragment* Sanitizer::SanitizeImpl( |
| ScriptState* script_state, |
| StringOrDocumentFragmentOrDocument& input, |
| ExceptionState& exception_state) { |
| DocumentFragment* fragment = nullptr; |
| |
| LocalDOMWindow* window = LocalDOMWindow::From(script_state); |
| if (!window) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Cannot find current DOM window."); |
| return nullptr; |
| } |
| if (input.IsDocumentFragment()) { |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIFromFragment); |
| fragment = input.GetAsDocumentFragment(); |
| } else if (input.IsString() || input.IsNull()) { |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIFromString); |
| |
| Document* document = |
| window->document() |
| ? window->document()->implementation().createHTMLDocument() |
| : DOMParser::Create(script_state) |
| ->parseFromString("<!DOCTYPE html><html><body></body></html>", |
| "text/html", |
| ParseFromStringOptions::Create()); |
| // TODO(https://crbug.com/1178774): Behavior difference need further |
| // investgate. |
| fragment = document->createRange()->createContextualFragment( |
| input.GetAsString(), exception_state); |
| } else if (input.IsDocument()) { |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIFromDocument); |
| |
| fragment = input.GetAsDocument()->createDocumentFragment(); |
| fragment->CloneChildNodesFrom(*(input.GetAsDocument()->body()), |
| CloneChildrenFlag::kClone); |
| } else { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Cannot find current DOM window."); |
| return nullptr; |
| } |
| |
| Node* node = fragment->firstChild(); |
| |
| while (node) { |
| // Skip non-Element nodes. |
| if (node->getNodeType() != Node::NodeType::kElementNode) { |
| node = NodeTraversal::Next(*node, fragment); |
| continue; |
| } |
| |
| // TODO(crbug.com/1126936): Review the sanitising algorithm for non-HTMLs. |
| // 1. Let |name| be |element|'s tag name. |
| String name = node->nodeName().UpperASCII(); |
| // 2. Classify elements into one of three kinds: kCustom, kUnknown, kRegular |
| ElementKind kind = ElementKind::kRegular; |
| if (CustomElement::IsValidName(AtomicString(name.LowerASCII()), false)) { |
| kind = ElementKind::kCustom; |
| } else if (IsA<HTMLElement>(node) && |
| To<HTMLElement>(node)->IsHTMLUnknownElement()) { |
| kind = ElementKind::kUnknown; |
| } |
| |
| // 3. If |kind| is `regular` and if |name| is not contained in the |
| // default element allow list, then 'drop' |
| if (kind == ElementKind::kRegular && |
| !default_allow_elements_.Contains(name)) { |
| node = DropElement(node, fragment); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } else if (kind == ElementKind::kCustom && !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 = DropElement(node, fragment); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } else if (drop_elements_.Contains(name)) { |
| // 5. If |name| is in |config|'s [=element drop list=] then 'drop'. |
| node = DropElement(node, fragment); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } else if (block_elements_.Contains(name)) { |
| // 6. If |name| is in |config|'s [=element block list=] then 'block'. |
| node = BlockElement(node, fragment, exception_state); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } else if (has_allow_elements_ && !allow_elements_.Contains(name)) { |
| // 7. if |config| has a non-empty [=element allow list=] and |name| is |
| // not in |config|'s [=element allow list=] then 'block'. |
| node = BlockElement(node, fragment, exception_state); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } else { |
| node = KeepElement(node, fragment, name, window); |
| } |
| } |
| |
| return fragment; |
| } |
| |
| // If the current element needs to be dropped, remove current element entirely |
| // and proceed to its next sibling. |
| Node* Sanitizer::DropElement(Node* node, DocumentFragment* 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(Node* node, |
| DocumentFragment* fragment, |
| ExceptionState& exception_state) { |
| Node* parent = node->parentNode(); |
| Node* next_sibling = node->nextSibling(); |
| while (node->hasChildren()) { |
| Node* n = node->firstChild(); |
| if (next_sibling) { |
| parent->insertBefore(n, next_sibling, exception_state); |
| } else { |
| parent->appendChild(n, exception_state); |
| } |
| if (exception_state.HadException()) { |
| return nullptr; |
| } |
| } |
| Node* tmp = node; |
| node = NodeTraversal::Next(*node, fragment); |
| tmp->remove(); |
| return node; |
| } |
| |
| // Remove any attributes to be dropped from the current element, and proceed to |
| // the next node (preorder, depth-first traversal). |
| Node* Sanitizer::KeepElement(Node* node, |
| DocumentFragment* fragment, |
| String& node_name, |
| LocalDOMWindow* window) { |
| Element* element = To<Element>(node); |
| if (has_allow_attributes_ && allow_attributes_.at("*").Contains(node_name)) { |
| } else if (drop_attributes_.at("*").Contains(node_name)) { |
| for (const auto& name : element->getAttributeNames()) { |
| element->removeAttribute(name); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } |
| } else { |
| for (const auto& name : element->getAttributeNames()) { |
| // Attributes in drop list or not in allow list while allow list |
| // exists will be dropped. |
| bool drop = (drop_attributes_.Contains(name) && |
| (drop_attributes_.at(name) == kVectorStar || |
| drop_attributes_.at(name).Contains(node_name))) || |
| (has_allow_attributes_ && |
| !(allow_attributes_.Contains(name) && |
| (allow_attributes_.at(name) == kVectorStar || |
| allow_attributes_.at(name).Contains(node_name)))); |
| if (drop) { |
| element->removeAttribute(name); |
| UseCounter::Count(window->GetExecutionContext(), |
| WebFeature::kSanitizerAPIActionTaken); |
| } |
| } |
| } |
| node = NodeTraversal::Next(*node, fragment); |
| return node; |
| } |
| |
| void Sanitizer::Trace(Visitor* visitor) const { |
| ScriptWrappable::Trace(visitor); |
| } |
| |
| } // namespace blink |