Commit 4df80911 authored by Yifan Luo's avatar Yifan Luo Committed by Commit Bot

[Sanitizer API] Add dropElements to config.

Bug: 1116418
Change-Id: If435e43ced3287398982aa2bc9719bc5498a40ff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2401162
Commit-Queue: Yifan Luo <lyf@chromium.org>
Reviewed-by: default avatarMike West <mkwst@chromium.org>
Reviewed-by: default avatarDaniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/master@{#806614}
parent 7db3923f
...@@ -62,7 +62,8 @@ class NodeTraversal { ...@@ -62,7 +62,8 @@ class NodeTraversal {
// Like next, but skips children and starts with the next sibling. // Like next, but skips children and starts with the next sibling.
CORE_EXPORT static Node* NextSkippingChildren(const Node&); CORE_EXPORT static Node* NextSkippingChildren(const Node&);
static Node* NextSkippingChildren(const Node&, const Node* stay_within); CORE_EXPORT static Node* NextSkippingChildren(const Node&,
const Node* stay_within);
static Node* FirstWithin(const Node& current) { return current.firstChild(); } static Node* FirstWithin(const Node& current) { return current.firstChild(); }
......
...@@ -4,13 +4,17 @@ ...@@ -4,13 +4,17 @@
#include "sanitizer.h" #include "sanitizer.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_node_filter.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_sanitizer_config.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_sanitizer_config.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h" #include "third_party/blink/renderer/core/dom/document_fragment.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/editing/serializers/serialization.h" #include "third_party/blink/renderer/core/editing/serializers/serialization.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.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/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink { namespace blink {
...@@ -20,7 +24,16 @@ Sanitizer* Sanitizer::Create(const SanitizerConfig* config, ...@@ -20,7 +24,16 @@ Sanitizer* Sanitizer::Create(const SanitizerConfig* config,
} }
Sanitizer::Sanitizer(const SanitizerConfig* config) Sanitizer::Sanitizer(const SanitizerConfig* config)
: config_(const_cast<SanitizerConfig*>(config)) {} : config_(const_cast<SanitizerConfig*>(config)) {
// Format dropElements to uppercases.
if (config->hasDropElements()) {
Vector<String> l;
for (const String& s : config->dropElements()) {
l.push_back(s.UpperASCII());
}
config_->setDropElements(l);
}
}
Sanitizer::~Sanitizer() = default; Sanitizer::~Sanitizer() = default;
...@@ -44,9 +57,38 @@ DocumentFragment* Sanitizer::sanitize(ScriptState* script_state, ...@@ -44,9 +57,38 @@ DocumentFragment* Sanitizer::sanitize(ScriptState* script_state,
DocumentFragment* fragment = document->createDocumentFragment(); DocumentFragment* fragment = document->createDocumentFragment();
DCHECK(document->QuerySelector("body")); DCHECK(document->QuerySelector("body"));
fragment->ParseHTML(input, document->QuerySelector("body")); fragment->ParseHTML(input, document->QuerySelector("body"));
// Remove all the elements in the dropElements list.
if (config_->hasDropElements()) {
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.
String node_name = node->nodeName();
// If the current element is dropped, remove current element entirely and
// proceed to its next sibling.
if (config_->dropElements().Contains(node_name.UpperASCII())) {
Node* tmp = node;
node = NodeTraversal::NextSkippingChildren(*node, fragment);
tmp->remove();
} else {
// Otherwise, proceed to the next node (preorder, depth-first
// traversal).
node = NodeTraversal::Next(*node, fragment);
}
}
}
return fragment; return fragment;
} }
// TODO(lyf): https://github.com/WICG/sanitizer-api/issues/34
SanitizerConfig* Sanitizer::creationOptions() const { SanitizerConfig* Sanitizer::creationOptions() const {
return config_; return config_;
} }
......
...@@ -32,6 +32,8 @@ class MODULES_EXPORT Sanitizer final : public ScriptWrappable { ...@@ -32,6 +32,8 @@ class MODULES_EXPORT Sanitizer final : public ScriptWrappable {
void Trace(Visitor*) const override; void Trace(Visitor*) const override;
private: private:
// TODO(lyf): Make config_ read-only. The creationOptions getter which
// asks for the pointer is forbidened by a read-only variable.
Member<SanitizerConfig> config_ = {}; Member<SanitizerConfig> config_ = {};
}; };
......
...@@ -4,4 +4,6 @@ ...@@ -4,4 +4,6 @@
// https://github.com/WICG/sanitizer-api // https://github.com/WICG/sanitizer-api
dictionary SanitizerConfig {}; dictionary SanitizerConfig {
sequence<DOMString>? dropElements;
};
...@@ -8,4 +8,6 @@ ...@@ -8,4 +8,6 @@
[CallWith=ScriptState, RaisesException] DOMString sanitizeToString(DOMString input); [CallWith=ScriptState, RaisesException] DOMString sanitizeToString(DOMString input);
}; };
dictionary SanitizerConfig {}; dictionary SanitizerConfig {
sequence<DOMString>? dropElements;
};
...@@ -8,52 +8,81 @@ ...@@ -8,52 +8,81 @@
<body> <body>
<script> <script>
const default_option ={dropElements: null};
test(t => { test(t => {
let s = new Sanitizer(); let s = new Sanitizer();
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator without config."); }, "SanitizerAPI creator without config.");
test(t => { test(t => {
let s = new Sanitizer({}); let s = new Sanitizer({});
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with empty config."); }, "SanitizerAPI creator with empty config.");
test(t => { test(t => {
let s = new Sanitizer(null); let s = new Sanitizer(null);
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with null as config."); }, "SanitizerAPI creator with null as config.");
test(t => { test(t => {
let s = new Sanitizer(undefined); let s = new Sanitizer(undefined);
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with undefined as config."); }, "SanitizerAPI creator with undefined as config.");
test(t => { test(t => {
let s = new Sanitizer({testConfig: [1,2,3], attr: ["test", "i", "am"]}); let s = new Sanitizer({testConfig: [1,2,3], attr: ["test", "i", "am"]});
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with config ignore unknown values."); }, "SanitizerAPI creator with config ignore unknown values.");
test(t => { test(t => {
let options = {}; let options = {dropElements: ["div"]};
let s = new Sanitizer(options); let s = new Sanitizer(options);
assert_true(s instanceof Sanitizer); assert_true(s instanceof Sanitizer);
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, {dropElements: ["DIV"]});
options.testConfig = [1,2,3]; options.dropElements.push("test");
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, {dropElements: ["DIV"]});
s.creationOptions = {testConfig: [1,2,3]}; s.creationOptions = {dropElements: ["test", "t"]};
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, {dropElements: ["DIV"]});
s.creationOptions['testConfig'] = [1,2,3]; s.creationOptions['dropElements'] = [1,2,3];
assert_object_equals(s.creationOptions, {}); assert_object_equals(s.creationOptions, {dropElements: ["DIV"]});
}, "SanitizerAPI config is not editable."); }, "SanitizerAPI config is not editable.");
test(t => {
let s = new Sanitizer({dropElements: []});
assert_true(s instanceof Sanitizer);
assert_equals(s.sanitizeToString("<div>balabala<i>test</i></div>"), "<div>balabala<i>test</i></div>");
}, "SanitizerAPI creator with config {dropElements: []}.")
test(t => {
let s = new Sanitizer({dropElements: null});
assert_true(s instanceof Sanitizer);
assert_true(s.creationOptions instanceof Object);
assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with config {dropElements: null}.")
test(t => {
let s = new Sanitizer({dropElements: undefined});
assert_true(s instanceof Sanitizer);
assert_true(s.creationOptions instanceof Object);
assert_object_equals(s.creationOptions, default_option);
}, "SanitizerAPI creator with config {dropElements: undefined}.");
test(t => {
assert_throws_js(TypeError, _ => {let s = new Sanitizer({dropElements: 123})});
}, "SanitizerAPI creator with config {dropElements: 123}.");
test(t => {
assert_throws_js(TypeError, _ => {let s = new Sanitizer({dropElements: "div"})});
}, "SanitizerAPI creator with config {dropElements: div}.");
</script> </script>
</body> </body>
</html> </html>
...@@ -13,5 +13,13 @@ PASS SanitizerAPI sanitize function for document ...@@ -13,5 +13,13 @@ PASS SanitizerAPI sanitize function for document
PASS SanitizerAPI sanitize function for html without close tag PASS SanitizerAPI sanitize function for html without close tag
FAIL SanitizerAPI sanitize function for scripts assert_equals: expected "" but got "<script>alert('i am a test')</script>" FAIL SanitizerAPI sanitize function for scripts assert_equals: expected "" but got "<script>alert('i am a test')</script>"
FAIL SanitizerAPI sanitize function for onclick scripts assert_equals: expected "<p>Click.</p>" but got "<p onclick=\"a= 123\">Click.</p>" FAIL SanitizerAPI sanitize function for onclick scripts assert_equals: expected "<p>Click.</p>" but got "<p onclick=\"a= 123\">Click.</p>"
PASS SanitizerAPI sanitize function for invalid config_input
PASS SanitizerAPI sanitize function for empty dropElements list
PASS SanitizerAPI sanitize function for test html without close tag with dropElements list ['div']
PASS SanitizerAPI sanitize function for test script with ["script"] as dropElements list
PASS SanitizerAPI sanitize function for dropElements list ["test", "i"]}
PASS SanitizerAPI sanitize function for dropElements list ["I", "AM"]}
PASS SanitizerAPI sanitize function for dropElements list ["am", "p"]}
PASS SanitizerAPI sanitize function for dropElements list with invalid values}
Harness: the test ran to completion. Harness: the test ran to completion.
...@@ -13,9 +13,12 @@ ...@@ -13,9 +13,12 @@
d.appendChild(fragment); d.appendChild(fragment);
return d.innerHTML; return d.innerHTML;
} }
const s = new Sanitizer({});
testcases.forEach(c => test(t => { testcases.forEach(c => test(t => {
let s = new Sanitizer(c.config_input);
assert_true(s.creationOptions instanceof Object);
assert_object_equals(s.creationOptions, c.config_value);
fragment = s.sanitize(c.value); fragment = s.sanitize(c.value);
assert_true(fragment instanceof DocumentFragment); assert_true(fragment instanceof DocumentFragment);
assert_equals(getString(fragment), c.result); assert_equals(getString(fragment), c.result);
......
...@@ -13,5 +13,13 @@ PASS SanitizerAPI sanitizeToString function for document ...@@ -13,5 +13,13 @@ PASS SanitizerAPI sanitizeToString function for document
PASS SanitizerAPI sanitizeToString function for html without close tag PASS SanitizerAPI sanitizeToString function for html without close tag
FAIL SanitizerAPI sanitizeToString function for scripts assert_equals: expected "" but got "<script>alert('i am a test')</script>" FAIL SanitizerAPI sanitizeToString function for scripts assert_equals: expected "" but got "<script>alert('i am a test')</script>"
FAIL SanitizerAPI sanitizeToString function for onclick scripts assert_equals: expected "<p>Click.</p>" but got "<p onclick=\"a= 123\">Click.</p>" FAIL SanitizerAPI sanitizeToString function for onclick scripts assert_equals: expected "<p>Click.</p>" but got "<p onclick=\"a= 123\">Click.</p>"
PASS SanitizerAPI sanitizeToString function for invalid config_input
PASS SanitizerAPI sanitizeToString function for empty dropElements list
PASS SanitizerAPI sanitizeToString function for test html without close tag with dropElements list ['div']
PASS SanitizerAPI sanitizeToString function for test script with ["script"] as dropElements list
PASS SanitizerAPI sanitizeToString function for dropElements list ["test", "i"]}
PASS SanitizerAPI sanitizeToString function for dropElements list ["I", "AM"]}
PASS SanitizerAPI sanitizeToString function for dropElements list ["am", "p"]}
PASS SanitizerAPI sanitizeToString function for dropElements list with invalid values}
Harness: the test ran to completion. Harness: the test ran to completion.
...@@ -8,9 +8,10 @@ ...@@ -8,9 +8,10 @@
<body> <body>
<script> <script>
const s = new Sanitizer({});
testcases.forEach(c => test(t => { testcases.forEach(c => test(t => {
let s = new Sanitizer(c.config_input);
assert_true(s.creationOptions instanceof Object);
assert_object_equals(s.creationOptions, c.config_value);
assert_equals(s.sanitizeToString(c.value), c.result); assert_equals(s.sanitizeToString(c.value), c.result);
}, "SanitizerAPI sanitizeToString function for " + c.message)); }, "SanitizerAPI sanitizeToString function for " + c.message));
</script> </script>
......
const testcases = [ const testcases = [
{value: "test", result: "test", message: "string"}, {config_input: {}, config_value: {dropElements: null}, value: "test", result: "test", message: "string"},
{value: "<b>bla</b>", result: "<b>bla</b>", message: "html fragment"}, {config_input: {}, config_value: {dropElements: null}, value: "<b>bla</b>", result: "<b>bla</b>", message: "html fragment"},
{value: "<a<embla", result: "", message: "broken html"}, {config_input: {}, config_value: {dropElements: null}, value: "<a<embla", result: "", message: "broken html"},
{value: {}, result: "[object Object]", message: "empty object"}, {config_input: {}, config_value: {dropElements: null}, value: {}, result: "[object Object]", message: "empty object"},
{value: 1, result: "1", message: "number"}, {config_input: {}, config_value: {dropElements: null}, value: 1, result: "1", message: "number"},
{value: 000, result: "0", message: "zeros"}, {config_input: {}, config_value: {dropElements: null}, value: 000, result: "0", message: "zeros"},
{value: 1+2, result: "3", message: "arithmetic"}, {config_input: {}, config_value: {dropElements: null}, value: 1+2, result: "3", message: "arithmetic"},
{value: "", result: "", message: "empty string"}, {config_input: {}, config_value: {dropElements: null}, value: "", result: "", message: "empty string"},
{value: undefined, result: "undefined", message: "undefined"}, {config_input: {}, config_value: {dropElements: null}, value: undefined, result: "undefined", message: "undefined"},
{value: null, result: "null", message: "null"}, {config_input: {}, config_value: {dropElements: null}, value: null, result: "null", message: "null"},
{value: "<html><head></head><body>test</body></html>", result: "test", message: "document"}, {config_input: {}, config_value: {dropElements: null}, value: "<html><head></head><body>test</body></html>", result: "test", message: "document"},
{value: "<div>test", result: "<div>test</div>", message: "html without close tag"}, {config_input: {}, config_value: {dropElements: null}, value: "<div>test", result: "<div>test</div>", message: "html without close tag"},
{value: "<script>alert('i am a test')<\/script>", result: "", message: "scripts"}, {config_input: {}, config_value: {dropElements: null}, value: "<script>alert('i am a test')<\/script>", result: "", message: "scripts"},
{value: "<p onclick='a= 123'>Click.</p>", result: "<p>Click.</p>", message: "onclick scripts"} {config_input: {}, config_value: {dropElements: null}, value: "<p onclick='a= 123'>Click.</p>", result: "<p>Click.</p>", message: "onclick scripts"},
{config_input: {test: 123}, config_value: {dropElements: null}, value: "test", result: "test", message: "invalid config_input"},
{config_input: {dropElements: []}, config_value: {dropElements:[]}, value: "test", result: "test", message: "empty dropElements list"},
{config_input: {dropElements: ["div"]}, config_value: {dropElements:["DIV"]}, value: "<div>test</div><c>bla", result: "<c>bla</c>", message: "test html without close tag with dropElements list ['div']"},
{config_input: {dropElements: ["script"]}, config_value: {dropElements:["SCRIPT"]}, value: "<script>alert('i am a test')<\/script>", result: "", message: "test script with [\"script\"] as dropElements list"},
{config_input: {dropElements: ["test", "i"]}, config_value: {dropElements:["TEST","I"]}, value: "<div>balabala<i>test</i></div><test>t</test>", result: "<div>balabala</div>", message: "dropElements list [\"test\", \"i\"]}"},
{config_input: {dropElements: ["I", "AM"]}, config_value: {dropElements:["I", "AM"]}, value: "<div>balabala<am>test</am></div>", result: "<div>balabala</div>", message: "dropElements list [\"I\", \"AM\"]}"},
{config_input: {dropElements: ["am", "p"]}, config_value: {dropElements:["AM","P"]}, value: "<div>balabala<i>i</i><p>t</p><test>a</test></div>", result: "<div>balabala<i>i</i><test>a</test></div>", message: "dropElements list [\"am\", \"p\"]}"},
{config_input: {dropElements: [123, [], "test", "i"]}, config_value: {dropElements:["123","","TEST","I"]}, value: "<div>balabala<i>test</i></div><test>t</test>", result: "<div>balabala</div>", message: "dropElements list with invalid values}"}
]; ];
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment