diff options
Diffstat (limited to 'testing/web-platform/tests/sanitizer-api')
13 files changed, 1217 insertions, 0 deletions
diff --git a/testing/web-platform/tests/sanitizer-api/META.yml b/testing/web-platform/tests/sanitizer-api/META.yml new file mode 100644 index 0000000000..7ac32665e1 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/META.yml @@ -0,0 +1,5 @@ +spec: https://wicg.github.io/sanitizer-api/ +suggested_reviewers: + - ivanlish + - mozfreddyb + - otherdaniel diff --git a/testing/web-platform/tests/sanitizer-api/WEB_FEATURES.yml b/testing/web-platform/tests/sanitizer-api/WEB_FEATURES.yml new file mode 100644 index 0000000000..428aeee63a --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: sanitizer + files: "**" diff --git a/testing/web-platform/tests/sanitizer-api/element-set-sanitized-html.https.html b/testing/web-platform/tests/sanitizer-api/element-set-sanitized-html.https.html new file mode 100644 index 0000000000..8fabf1ce29 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/element-set-sanitized-html.https.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="support/testcases.sub.js"></script> +</head> +<body> +<script> + function buildNode(element_name, markup) { + const e = document.createElement(element_name); + e.innerHTML = markup; + return e; + } + + function assert_node_equals(node1, node2) { + assert_equals(node2.innerHTML, node1.innerHTML); + assert_true(node1.isEqualNode(node2), + `Node[${node1.innerHTML}] == Node[${node2.innerHTML}]`); + // TODO(https://github.com/WICG/sanitizer-api/issues/202) + /* + if (node1 instanceof HTMLTemplateElement) { + assert_true(node1.content.isEqualNode(node2.content), "<template> content is equal"); + } + */ + } + + for (const context of ["script", "iframe", "object", "div"]) { + const should_fail = context != "div"; + test(t => { + let elem = document.createElement(context); + let probe_fn = _ => elem.setHTML("<div>Hello!</div>"); + if (should_fail) { + assert_throws_js(TypeError, probe_fn); + } else { + probe_fn(); + } + assert_equals(should_fail, !elem.firstChild); + }, `${context}.setHTML should ${should_fail ? "fail" : "pass"}.`); + } + + for (const context of ["div", "template", "table"]) { + const elem1 = document.createElement(context); + const elem2 = document.createElement(context); + for (const probe of ["<em>Hello</em>", "<td>data</td>"]) { + elem1.setHTML(probe, {}); + elem2.innerHTML = probe; + test(t => { + assert_node_equals(elem2, elem1); + }, `Sanitizer: <${context}>.setHTML("${probe}", ...) obeys parse context.`); + } + } + + for (const testcase of testcases) { + const element = document.createElement("template"); + test(t => { + element.setHTML(testcase.value, {sanitizer: testcase.config_input }); + assert_node_equals(buildNode(element.localName, testcase.result), element); + }, "Sanitizer: Element.setHTML with config: " + testcase.message); + } + + [ + undefined, + {}, + { sanitizer: {} }, + { sanitizer: undefined }, + { avocado: {} }, + ].forEach((options, index) => { + test(t => { + const e = document.createElement("div"); + e.setHTML("<em>bla</em><script>bla<" + "/script>", options); + assert_equals(e.innerHTML, "<em>bla</em>"); + }, `Sanitizer: Element.setHTML options dictionary #${index} uses default.`); + }); + + [ + "tomato", + { sanitizer: false }, + { sanitizer: "avocado" }, + ].forEach((options, index) => { + test(t => { + assert_throws_js(TypeError, _ => { + document.createElement("div").setHTML("bla", options); + }); + }, `Sanitizer: Element.setHTML invalid options dictionary #${index}`); + }); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/idlharness.https.window.js b/testing/web-platform/tests/sanitizer-api/idlharness.https.window.js new file mode 100644 index 0000000000..384317b8e5 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/idlharness.https.window.js @@ -0,0 +1,12 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +idl_test( + ['sanitizer-api.tentative'], + ['html'], + idl_array => { + idl_array.add_objects({ + Sanitizer: ['new Sanitizer({})'] + }); + } +); diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-config.https.html b/testing/web-platform/tests/sanitizer-api/sanitizer-config.https.html new file mode 100644 index 0000000000..fc371b5bb9 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-config.https.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> + +<body> +<script> + test(t => { + let s = new Sanitizer(); + assert_true(s instanceof Sanitizer); + }, "SanitizerAPI creator without config."); + + test(t => { + let s = new Sanitizer({}); + assert_true(s instanceof Sanitizer); + }, "SanitizerAPI creator with empty config."); + + test(t => { + let s = new Sanitizer(null); + assert_true(s instanceof Sanitizer); + }, "SanitizerAPI creator with null as config."); + + test(t => { + let s = new Sanitizer(undefined); + assert_true(s instanceof Sanitizer); + }, "SanitizerAPI creator with undefined as config."); + + test(t => { + let s = new Sanitizer({testConfig: [1,2,3], attr: ["test", "i", "am"]}); + assert_true(s instanceof Sanitizer); + }, "SanitizerAPI creator with config ignore unknown values."); + + // In-depth testing of sanitization is handled in other tests. Here we + // do presence testing for each of the config options and test 3 things: + // - One case where our test string is modified, + // - one where it's unaffected, + // - that a config can't be changed afterwards. + // (I.e., that the Sanitizer won't hold on to a reference of the options.) + + // The probe determines whether the Sanitizer modifies the probe string. + const probe_string = "<div id=\"i\">balabala</div><p>test</p>"; + const probe = sanitizer => { + let template = document.createElement("template"); + template.innerHTML = probe_string; + let fragment = sanitizer.sanitize(template.content); + let div = document.createElement("div"); + div.append(fragment); + return probe_string == div.innerHTML; + }; + + const should_stay_the_same = { + elements: [ "div", "p" ], + replaceWithChildrenElements: [ "test" ], + removeElements: [ "test" ], + attributes: ["id"], + removeAttributes: ["bla"], + }; + const should_modify = { + elements: [ "div", "span" ], + replaceWithChildrenElements: [ "div" ], + removeElements: [ "p" ], + attributes: ["test"], + removeAttributes: ["id"], + }; + + assert_array_equals(Object.keys(should_stay_the_same), Object.keys(should_modify)); + Object.keys(should_stay_the_same).forEach(option_key => { + test(t => { + const options = {}; + options[option_key] = should_stay_the_same[option_key]; + const s = new Sanitizer(options); + assert_true(s instanceof Sanitizer); + assert_true(probe(s)); + }, `SanitizerAPI: ${option_key} stays is okay.`); + + const options = {}; + options[option_key] = should_modify[option_key]; + const s = new Sanitizer(options); + test(t => { + assert_true(s instanceof Sanitizer); + assert_false(probe(s)); + }, `SanitizerAPI: ${option_key} modify is okay.`); + + options[option_key] = should_stay_the_same[option_key]; + test(t => { + assert_false(probe(s)); + }, `SanitizerAPI: ${option_key} config is not kept as reference.`); + }); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-insecure-context.html b/testing/web-platform/tests/sanitizer-api/sanitizer-insecure-context.html new file mode 100644 index 0000000000..4b185fd3a7 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-insecure-context.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + // Currently, the Sanitizer requires a secure context. + test(t => { + assert_false(globalThis.isSecureContext); + assert_equals("Sanitizer" in globalThis, globalThis.isSecureContext); + assert_equals("setHTML" in document.body, globalThis.isSecureContext); + }, "Sanitizer API in an insecure context."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-names.https.html b/testing/web-platform/tests/sanitizer-api/sanitizer-names.https.html new file mode 100644 index 0000000000..cd33bbc763 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-names.https.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + // Like assert_array_equals, but disregard element order. + function assert_array_same(actual, expected) { + assert_array_equals(actual.sort(), expected.sort()); + } + + // Element names: + const elems_valid = [ + "p", "template", "span", "custom-elements", "potato", + // Arguments will be stringified, so anything that stringifies to a valid + // name is also valid. + 123 + ]; + const elems_invalid = [ + "", {name: ""}, + ]; + + // Attribute names: + const attrs_valid = [ + "href", "span", + ]; + const attrs_invalid = [ + ]; + + const all_elems = elems_valid.concat(elems_invalid); + const all_attrs = attrs_valid.concat(attrs_invalid); + for (const item of ["elements", "removeElements", "replaceWithChildrenElements"]) { + test(t => { + const sanitizer = new Sanitizer({[item]: all_elems}); + assert_array_same(sanitizer.getConfiguration()[item], + elems_valid.map(x => "" + x)); + }, `Element names in config item: ${item}`); + } + for (const item of ["attributes", "removeAttributes"]) { + test(t => { + const sanitizer = new Sanitizer({[item]: all_attrs}); + assert_array_same(Object.keys(sanitizer.getConfiguration()[item]), + attrs_valid.map(x => "" + x)); + }, `Attribute names in config item: ${item}`); + } + + // Quick sanity tests for namespaced elements. + // Each test case is a duo or triplet: + // - a Sanitizer config string for an element. + // - an HTML probe string. + // - the expected result. (If different from the probe.) + const SVG_NS = "http://www.w3.org/2000/svg"; + const MATHML_NS = "http://www.w3.org/1998/Math/MathML"; + [ + [ "p", "<p>Hello</p>" ], + [ "svg", "<svg>Hello</svg>", "Hello" ], + [ { name: "svg", namespace: SVG_NS }, "<svg>Hello</svg>" ], + [ "math", "<math>Hello</math>", "Hello" ], + [ { name: "math", namespace: SVG_NS }, "<math>Hello</math>", "Hello" ], + [ { name: "math", namespace: MATHML_NS }, "<math>Hello</math>" ], + ].forEach(([elem, probe, expected], index) => { + test(t => { + const options = { elements: [elem], + // TODO(https://github.com/WICG/sanitizer-api/issues/167) + unknownMarkup: true}; + const template = document.createElement("template"); + template.setHTML(probe, {sanitizer: options}); + assert_equals(template.innerHTML, expected ?? probe); + }, `Namespaced elements #${index}: elements: [${JSON.stringify(elem)}]`); + }); + + // Same for attributes: + const XLINK_NS = "http://www.w3.org/1999/xlink"; + [ + [ { name: "style"}, "<p style=\"bla\"></p>" ], + [ { name: "href"}, "<p href=\"bla\"></p>" ], + [ { name: "xlink:href"}, "<p xlink:href=\"bla\"></p>" ], + [ { name: "href", namespace: XLINK_NS}, "<p xlink:href=\"bla\"></p>", "<p></p>" ], + [ { name: "href", namespace: XLINK_NS}, "<p href='bla'></p>", "<p></p>" ], + [ { name: "href"}, "<p xlink:href='bla'></p>", "<p></p>" ], + ].forEach(([attr, probe, expected], index) => { + test(t => { + const options = {attributes: [attr], + // TODO(https://github.com/WICG/sanitizer-api/issues/167) + unknownMarkup: true}; + const template = document.createElement("template"); + template.setHTML(probe, {sanitizer: options}); + assert_equals(template.innerHTML, expected ?? probe); + }, `Namespaced attributes #${index}: attributes: [${JSON.stringify(attr)}]`); + }); + + // Test for namespaced attribute inside namespace element + test(t => { + const probe = `<svg><a xlink:href="bla"></a></svg>`; + + const options = { + elements: [ + {name: "svg", namespace: SVG_NS}, + {name: "a", namespace: SVG_NS, attributes: [ + { name: "href", namespace: XLINK_NS } + ]} + ], + // TODO(https://github.com/WICG/sanitizer-api/issues/167) + unknownMarkup: true}; + const template = document.createElement("template"); + template.setHTML(probe, {sanitizer: options}); + assert_equals(template.innerHTML, probe); + }, "Namespaced attribute xlink:href inside SVG tree"); + + // Most element and attribute names are lower-cased, but "foreign content" + // like SVG and MathML have some mixed-cased names. + [ + [ "feBlend", "<feBlend></feBlend>" ], + [ "feColorMatrix", "<feColorMatrix></feColorMatrix>" ], + [ "textPath", "<textPath></textPath>" ], + ].forEach(([elem, probe], index) => { + const sanitize = (elem, probe) => { + const options = {elements: [ + { name: "svg", namespace: SVG_NS }, + { name: elem, namespace: SVG_NS } + ], + // TODO(https://github.com/WICG/sanitizer-api/issues/167) + unknownMarkup: true}; + const template = document.createElement("template"); + template.setHTML(`<svg>${probe}</svg>`, {sanitizer: options}); + return template.content.firstElementChild.innerHTML; + }; + test(t => { + assert_equals(sanitize(elem, probe), probe); + }, `Mixed-case element names #${index}: "svg:${elem}"`); + test(t => { + // Lowercase element names should be normalized to mixed-case. + assert_equals(sanitize(elem.toLowerCase(), probe), probe); + }, `Lower-case element names #${index}: "svg:${elem.toLowerCase()}"`); + test(t => { + assert_not_equals(sanitize(elem.toUpperCase(), probe), probe); + }, `Upper-case element names #${index}: "svg:${elem.toUpperCase()}"`); + test(t => { + const elems = ["svg:" + elem]; + assert_array_equals( + new Sanitizer({elements: elems}).getConfiguration().allowElements, + elems); + }, `Mixed case element names #${index}: "${elem}" is preserved in config.`); + }); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-query-config.https.html b/testing/web-platform/tests/sanitizer-api/sanitizer-query-config.https.html new file mode 100644 index 0000000000..60cba2d618 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-query-config.https.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + function assert_deep_equals(obj1, obj2) { + assert_equals(typeof obj1, typeof obj2); + if (typeof obj1 == "string") { + assert_equals(obj1, obj2); + } else if (typeof obj1 == "boolean") { + assert_true(obj1 == obj2); + } else if (Array.isArray(obj1)) { + assert_equals(obj1.length, obj2.length); + assert_array_equals(obj1.sort(), obj2.sort()); + } else if (typeof obj1 == "object") { + assert_array_equals(Object.keys(obj1).sort(), Object.keys(obj2).sort()); + for (const k of Object.keys(obj1)) + assert_deep_equals(obj1[k], obj2[k]); + } + } + + test(t => { + // Quick sanity test: Test a few default values. + assert_in_array("div", Sanitizer.getDefaultConfiguration().allowElements); + assert_false(Sanitizer.getDefaultConfiguration().allowElements.includes("script")); + assert_false(Sanitizer.getDefaultConfiguration().allowElements.includes("noscript")); + + assert_true("span" in Sanitizer.getDefaultConfiguration().allowAttributes); + assert_false("onclick" in Sanitizer.getDefaultConfiguration().allowAttributes); + + assert_false("dropElements" in Sanitizer.getDefaultConfiguration()); + assert_false("blockElements" in Sanitizer.getDefaultConfiguration()); + assert_false("dropAttributes" in Sanitizer.getDefaultConfiguration()); + assert_false(Sanitizer.getDefaultConfiguration().allowCustomElements); + assert_false(Sanitizer.getDefaultConfiguration().allowUnknownMarkup); + }, "SanitizerAPI getDefaultConfiguration()"); + + test(t => { + assert_deep_equals(Sanitizer.getDefaultConfiguration(), + new Sanitizer().getConfiguration()); + }, "SanitizerAPI getConfiguration() on default created Sanitizer"); + + test(t => { + const configs = [{ + allowElements: ["div", "span", "helloworld"], + dropElements: ["xxx"], + allowAttributes: { "class": ["*"], "color": ["span", "div"], + "onclick": ["*"] }, + allowCustomElements: true, + allowUnknownMarkup: true, + },{ + blockElements: ["table", "tbody", "th", "td"], + }, { + allowCustomElements: false, + }, { + allowUnknownMarkup: false, + }]; + for (const config of configs) + assert_deep_equals(config, new Sanitizer(config).getConfiguration()); + + // Also test a mixed case variant: + const config_0_mixed = { + allowElements: ["div", "sPAn", "HelloWorld"], + dropElements: ["XXX"], + allowAttributes: { "class": ["*"], "color": ["sPAn", "div"], + "onclick": ["*"] }, + allowCustomElements: true, + allowUnknownMarkup: true, + }; + assert_deep_equals(config_0_mixed, + new Sanitizer(config_0_mixed).getConfiguration()); + }, "SanitizerAPI getConfiguration() reflects creation config."); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-sanitize.https.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-sanitize.https.tentative.html new file mode 100644 index 0000000000..82eaeb4832 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-sanitize.https.tentative.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="support/testcases.sub.js"></script> +</head> + +<body> +<script> + function getString(fragment) { + d = document.createElement("div"); + d.replaceChildren(...fragment.cloneNode(true).childNodes); + return d.innerHTML; + } + + function getFragment(markup) { + const d = document.createElement("div"); + d.innerHTML = markup; + let f = document.createDocumentFragment(); + f.replaceChildren(...d.childNodes); + return f; + } + function getDoc(markup) { + return new DOMParser().parseFromString(markup, "text/html"); + } + function assert_node_equals(node1, node2) { + assert_true(node1 instanceof Node && node1.isEqualNode(node2), + `Node[${getString(node1)}] == Node[${getString(node2)}]`); + } + + test(t => { + let s = new Sanitizer(); + assert_throws_js(TypeError, _ => s.sanitize()); + }, "Sanitizer.sanitize() should throw an error."); + + test(t => { + let s = new Sanitizer(); + assert_throws_js(TypeError, _ => s.sanitize(null)); + }, "Sanitizer.sanitize(null)."); + + const probe_string = `<a href="about:blank">hello</a><script>cons` + + `ole.log("world!");<` + `/script>`; + test(t => { + const sanitized = new Sanitizer().sanitize(getFragment(probe_string)); + const expected = getFragment(probe_string.substr(0, 31)); + assert_node_equals(expected, sanitized); + }, "Sanitizer.sanitze(DocumentFragment)"); + + test(t => { + const sanitized = new Sanitizer().sanitize(getDoc(probe_string)); + const expected = getFragment(probe_string.substr(0, 31)); + assert_node_equals(expected, sanitized); + }, "Sanitizer.sanitze(Document)"); + + testcases.forEach(c => test(t => { + let s = new Sanitizer(c.config_input); + var dom = new DOMParser().parseFromString("<!DOCTYPE html><body>" + c.value, "text/html"); + fragment = s.sanitize(dom); + assert_true(fragment instanceof DocumentFragment); + + let result = getString(fragment); + assert_equals(result, c.result); + }, "SanitizerAPI with config: " + c.message + ", sanitize from document function for <body>")); + + testcases.forEach(c => test(t => { + let s = new Sanitizer(c.config_input); + let tpl = document.createElement("template"); + tpl.innerHTML = c.value; + fragment = s.sanitize(tpl.content); + assert_true(fragment instanceof DocumentFragment); + assert_equals(getString(fragment), c.result); + }, "SanitizerAPI with config: " + c.message + ", sanitize from document fragment function for <template>")); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-sanitizeFor.https.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-sanitizeFor.https.tentative.html new file mode 100644 index 0000000000..77ae0abb6b --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-sanitizeFor.https.tentative.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="support/testcases.sub.js"></script> +</head> + +<body> +<script> + function buildNode(element_name, markup) { + const e = document.createElement(element_name); + e.innerHTML = markup; + return e; + } + + function toString(node) { + const e = document.createElement("div"); + e.append(node.cloneNode(true)); + return e.innerHTML; + } + + function assert_node_equals(node1, node2) { + assert_equals(node1 instanceof Node, node2 instanceof Node); + if (!(node1 instanceof Node)) return; + + node1.normalize(); + node2.normalize(); + assert_true(node1.isEqualNode(node2), + `Node[${toString(node1)}] == Node[${toString(node2)}]`); + if (node1 instanceof HTMLTemplateElement) { + assert_node_equals(node1.content, node2.content); + } + } + + test(t => { + let s = new Sanitizer(); + assert_throws_js(TypeError, _ => s.sanitizeFor()); + assert_throws_js(TypeError, _ => s.sanitizeFor(null)); + }, "Sanitizer.sanitizeFor() should throw."); + + test(t => { + let s = new Sanitizer(); + assert_throws_js(TypeError, _ => s.sanitizeFor("xxx")); + }, "Sanitizer.sanitizeFor() with one argument should throw."); + + for (const context of ["script", "iframe", "object", "div"]) { + const should_fail = context != "div"; + test(t => { + let result = new Sanitizer().sanitizeFor(context, "<div>Hello!</div>"); + if (should_fail) { + assert_equals(null, result); + } else { + assert_true(result instanceof HTMLElement); + } + }, `Sanitizer.sanitizeFor("${context}", ...) should ${should_fail ? "fail" : "pass"}.`); + } + + async_test(t => { + let s = new Sanitizer(); + s.sanitizeFor("div", "<img src='https://bla/'>"); + t.step_timeout(_ => { + assert_equals(performance.getEntriesByName("https://bla/").length, 0); + t.done(); + }, 1000); + }, "Sanitizer.sanitizeFor function shouldn't load the image."); + + test(t => { + const probe = `<a href="about:blank">hello</a><script>con` + + `sole.log("world!");<` + `/script>`; + const expected = `<a href="about:blank">hello</a>`; + for (const element of ["div", "template", "span", "table", "td", + "pumuckl", "custom-element", "linearGradient", + "svg", "svg:img", "svg:linearGradient"]) { + assert_node_equals( + buildNode(element, expected), + new Sanitizer().sanitizeFor(element, probe)); + } + }, `Sanitizer.sanitizeFor(element, ..)`); + + for (const context of ["div", "template", "table"]) { + for (const probe of ["<em>Hello</em>", "<td>data</td>"]) { + test(t => { + assert_node_equals( + buildNode(context, probe), + new Sanitizer().sanitizeFor(context, probe)); + }, `Sanitizer.sanitizeFor("${context}", "${probe}") obeys parse context.`); + } + } + + for (const testcase of testcases) { + test(t => { + let s = new Sanitizer(testcase.config_input); + assert_node_equals( + buildNode("template", testcase.result), + s.sanitizeFor("template", testcase.value)); + }, "Sanitizer.sanitizeFor with config: " + testcase.message); + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-secure-context.https.html b/testing/web-platform/tests/sanitizer-api/sanitizer-secure-context.https.html new file mode 100644 index 0000000000..0e04e04d16 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-secure-context.https.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + // Currently, the Sanitizer requires a secure context. + test(t => { + assert_true(globalThis.isSecureContext); + assert_equals("Sanitizer" in globalThis, globalThis.isSecureContext); + assert_equals("setHTML" in document.body, globalThis.isSecureContext); + }, "SanitizerAPI in a secure context."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-unknown.https.html b/testing/web-platform/tests/sanitizer-api/sanitizer-unknown.https.html new file mode 100644 index 0000000000..7b9835cb6d --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/sanitizer-unknown.https.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +test(t => { + d = document.createElement("div") + d.setHTML("<hello><world>", + { sanitizer: { elements: ["hello", "world"] } }); + assert_equals(d.innerHTML, ""); +}, "Unknown element names get blocked without unknownMarkup."); + +test(t => { + d = document.createElement("div") + d.setHTML("<hello><world>", + { sanitizer: { unknownMarkup: true, elements: ["hello", "world"] } }); + assert_equals(d.innerHTML, "<hello><world></world></hello>"); +}, "Unknown element names pass with unknownMarkup."); + +test(t => { + d = document.createElement("div") + d.setHTML("<b hello='1' world>", + { sanitizer: { attributes: ["name", "world"] } }); + assert_equals(d.innerHTML, "<b></b>"); +}, "Unknown attributes names get blocked without unknownMarkup."); + +test(t => { + d = document.createElement("div") + d.setHTML("<b hello='1' world>", + { sanitizer: { unknownMarkup: true, attributes: ["name", "world"] } }); + assert_equals(d.innerHTML, `<b hello="1" world=""></b>`); +}, "Unknown attribute names pass with unknownMarkup."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/sanitizer-api/support/testcases.sub.js b/testing/web-platform/tests/sanitizer-api/support/testcases.sub.js new file mode 100644 index 0000000000..7ce755ca06 --- /dev/null +++ b/testing/web-platform/tests/sanitizer-api/support/testcases.sub.js @@ -0,0 +1,538 @@ +const testcases = [ + { config_input: {}, value: "test", result: "test", message: "string" }, + { + config_input: {}, + value: "<b>bla</b>", + result: "<b>bla</b>", + message: "html fragment", + }, + { config_input: {}, value: "<a<embla", result: "", message: "broken html" }, + { + config_input: {}, + value: {}, + result: "[object Object]", + message: "empty object", + }, + { config_input: {}, value: 1, result: "1", message: "number" }, + { config_input: {}, value: 000, result: "0", message: "zeros" }, + { config_input: {}, value: 1 + 2, result: "3", message: "arithmetic" }, + { config_input: {}, value: "", result: "", message: "empty string" }, + { + config_input: {}, + value: undefined, + result: "undefined", + message: "undefined", + }, + { + config_input: {}, + value: "<html><head></head><body>test</body></html>", + result: "test", + message: "document", + }, + { + config_input: {}, + value: "<div>test", + result: "<div>test</div>", + message: "html without close tag", + }, + { + config_input: {}, + value: "<script>alert('i am a test')</script>", + result: "", + message: "scripts for default configs", + }, + { + config_input: {}, + value: "hello<script>alert('i am a test')</script>", + result: "hello", + message: "script not as root", + }, + { + config_input: {}, + value: "<div><b>hello<script>alert('i am a test')</script>", + result: "<div><b>hello</b></div>", + message: "script deeper in the tree", + }, + { + config_input: {}, + value: "<p onclick='a= 123'>Click.</p>", + result: "<p>Click.</p>", + message: "onclick scripts", + }, + { + config_input: {}, + value: "<plaintext><p>text</p>", + result: "<p>text</p>", + message: "plaintext", + }, + { + config_input: {}, + value: "<xmp>TEXT</xmp>", + result: "TEXT", + message: "xmp", + }, + { + config_input: { test: 123 }, + value: "test", + result: "test", + message: "invalid config_input", + }, + { + config_input: { removeElements: [] }, + value: "test", + result: "test", + message: "empty removeElements list", + }, + { + config_input: { removeElements: ["div"] }, + value: "<div>test</div><p>bla", + result: "<p>bla</p>", + message: "test html without close tag with removeElements list ['div']", + }, + { + config_input: {}, + value: "<custom-element>test</custom-element>bla", + result: "bla", + message: "default behavior for custom elements", + }, + { + config_input: { customElements: true }, + value: "<custom-element>test</custom-element>bla", + result: "testbla", + message: "allow custom elements", + }, + { + config_input: { + customElements: true, + elements: ["custom-element"], + }, + value: "<custom-element>test</custom-element>bla", + result: "<custom-element>test</custom-element>bla", + message: "allow custom elements with allow elements", + }, + { + config_input: { customElements: false }, + value: "<custom-element>test</custom-element>bla", + result: "bla", + message: "disallow custom elements", + }, + { + config_input: { + removeElements: ["custom-element"], + customElements: true, + }, + value: "<custom-element>test</custom-element>bla", + result: "bla", + message: 'allow custom elements with drop list contains ["custom-element"]', + }, + { + config_input: { removeElements: ["script"] }, + value: "<script>alert('i am a test')</script>", + result: "", + message: 'test script with ["script"] as removeElements list', + }, + { + config_input: { removeElements: ["test-element", "i"] }, + value: "<div>balabala<i>test</i></div><test-element>t</test-element>", + result: "<div>balabala</div>", + message: 'removeElements list ["test-element", "i"]}', + }, + { + config_input: { removeElements: ["dl", "p"] }, + value: "<div>balabala<i>i</i><p>t</p></div>", + result: "<div>balabala<i>i</i></div>", + message: 'removeElements list ["dl", "p"]}', + }, + { + config_input: { elements: ["p"] }, + value: "<div>test<div>p</div>tt<p>div</p></div>", + result: "testptt<p>div</p>", + message: 'elements list ["p"]', + }, + { + config_input: { removeElements: ["div"], elements: ["div"] }, + value: "<div>test</div><p>bla", + result: "bla", + message: "elements list has no influence to removeElements", + }, + { + config_input: { removeAttributes: [] }, + value: "<p id='test'>Click.</p>", + result: '<p id="test">Click.</p>', + message: "empty removeAttributes list with id attribute", + }, + { + config_input: { removeAttributes: ["id"] }, + value: "<p id='test'>Click.</p>", + result: "<p>Click.</p>", + message: 'removeAttributes list ["id"] with id attribute', + }, + { + config_input: { + removeAttributes: ["data-attribute-with-dashes"], + }, + value: + "<p id='p' data-attribute-with-dashes='123'>Click.</p><script>document.getElementById('p').dataset.attributeWithDashes=123;</script>", + result: '<p id="p">Click.</p>', + message: + 'removeAttributes list ["data-attribute-with-dashes"] with dom dataset js access', + }, + { + config_input: { + elements: [ + { name: "p", attributes: ["title"] }, + { name: "div", attributes: ["id"] }, + ], + }, + value: "<p id='p' title='p'>P</p><div id='div' title='div'>DIV</div>", + result: '<p title="p">P</p><div id="div">DIV</div>', + message: + 'elements list with <p> attributes: ["title"] and div attributes: ["id"] lists', + }, + { + config_input: { + elements: [ + { name: "p", removeAttributes: ["title"] }, + { name: "div", removeAttributes: ["id"] }, + ], + }, + value: "<p id='p' title='p'>P</p><div id='div' title='div'>DIV</div>", + result: '<p id="p">P</p><div title="div">DIV</div>', + message: + 'elements list with <p> removeAttributes: ["title"] and div removeAttributes: ["id"] lists', + }, + { + config_input: { + elements: [{ name: "div", attributes: ["id"], removeAttributes: ["id"] }], + }, + value: "<div id='div' title='div'>DIV</div>", + result: "<div>DIV</div>", + message: + 'elements list with <div> attributes: ["id"] and removeAttributes: ["id"] lists', + }, + { + config_input: { + elements: [{ name: "div", attributes: ["id", "title"] }], + attributes: [] + }, + value: "<div id='div' title='div'>DIV</div>", + result: "<div>DIV</div>", + message: + 'elements list with <div> attributes: ["id", "title"] does not override empty attributes: [] list', + }, + { + config_input: { + elements: [{ name: "div", attributes: ["id", "title"] }], + removeAttributes: ["id", "title"] + }, + value: "<div id='div' title='div'>DIV</div>", + result: "<div>DIV</div>", + message: + 'elements list with <div> attributes: ["id", "title"] does not override removeAttributes: ["id", "title"] list', + }, + { + config_input: { + elements: [{ name: "div", removeAttributes: ["id", "title"] }], + attributes: ["id", "title"] + }, + value: "<div id='div' title='div'>DIV</div>", + result: "<div>DIV</div>", + message: + 'elements list with <div> removeAttributes: ["id", "title"] is effective even with attributes: ["id", "title"] list', + }, + { + config_input: { attributes: ["id"] }, + value: "<p id='test' onclick='a= 123'>Click.</p>", + result: '<p id="test">Click.</p>', + message: 'attributes list ["id"] with id attribute and onclick scripts', + }, + // {config_input: {allowAttributes: {"*": ["a"]}}, value: "<a id='a' style='color: black'>Click.</a><div style='color: white'>div</div>", result: "<a id=\"a\" style=\"color: black\">Click.</a><div>div</div>", message: "allowAttributes list {\"*\": [\"a\"]} with style attribute"}, + { + config_input: { + removeAttributes: ["style"], + attributes: ["style"], + }, + value: "<p style='color: black'>Click.</p>", + result: "<p>Click.</p>", + message: "attributes list has no influence to removeAttributes list", + }, + { + config_input: { elements: ["template", "div"] }, + value: "<template><script>test</script><div>hello</div></template>", + result: "<template><div>hello</div></template>", + message: "Template element", + }, + { + config_input: {}, + value: "<a href='javascript:evil.com'>Click.</a>", + result: "<a>Click.</a>", + message: "HTMLAnchorElement with javascript protocal", + }, + { + config_input: {}, + value: "<a href=' javascript:evil.com'>Click.</a>", + result: "<a>Click.</a>", + message: "HTMLAnchorElement with javascript protocal start with space", + }, + { + config_input: {}, + value: "<a href='http:evil.com'>Click.</a>", + result: '<a href="http:evil.com">Click.</a>', + message: "HTMLAnchorElement", + }, + { + config_input: {}, + value: "<area href='javascript:evil.com'>Click.</area>", + result: "<area>Click.", + message: "HTMLAreaElement with javascript protocal", + }, + { + config_input: {}, + value: "<area href=' javascript:evil.com'>Click.</area>", + result: "<area>Click.", + message: "HTMLAreaElement with javascript protocal start with space", + }, + { + config_input: {}, + value: "<area href='http:evil.com'>Click.</area>", + result: '<area href="http:evil.com">Click.', + message: "HTMLAreaElement", + }, + { + config_input: {}, + value: "<form action='javascript:evil.com'>Click.</form>", + result: "<form>Click.</form>", + message: "HTMLFormElement with javascript action", + }, + { + config_input: {}, + value: "<form action=' javascript:evil.com'>Click.</form>", + result: "<form>Click.</form>", + message: "HTMLFormElement with javascript action start with space", + }, + { + config_input: {}, + value: "<form action='http:evil.com'>Click.</form>", + result: '<form action="http:evil.com">Click.</form>', + message: "HTMLFormElement", + }, + { + config_input: {}, + value: "<input formaction='javascript:evil.com'>Click.</input>", + result: "<input>Click.", + message: "HTMLInputElement with javascript formaction", + }, + { + config_input: {}, + value: "<input formaction=' javascript:evil.com'>Click.</input>", + result: "<input>Click.", + message: "HTMLInputElement with javascript formaction start with space", + }, + { + config_input: {}, + value: "<input formaction='http:evil.com'>Click.</input>", + result: '<input formaction="http:evil.com">Click.', + message: "HTMLInputElement", + }, + { + config_input: {}, + value: "<button formaction='javascript:evil.com'>Click.</button>", + result: "<button>Click.</button>", + message: "HTMLButtonElement with javascript formaction", + }, + { + config_input: {}, + value: "<button formaction=' javascript:evil.com'>Click.</button>", + result: "<button>Click.</button>", + message: "HTMLButtonElement with javascript formaction start with space", + }, + { + config_input: {}, + value: "<button formaction='http:evil.com'>Click.</button>", + result: '<button formaction="http:evil.com">Click.</button>', + message: "HTMLButtonElement", + }, + { + config_input: {}, + value: + "<p>Some text</p></body><!-- 1 --></html><!-- 2 --><p>Some more text</p>", + result: "<p>Some text</p><p>Some more text</p>", + message: "malformed HTML", + }, + { + config_input: {}, + value: "<p>Some text</p><!-- 1 --><!-- 2 --><p>Some more text</p>", + result: "<p>Some text</p><p>Some more text</p>", + message: "HTML with comments; comments not allowed", + }, + { + config_input: { comments: true }, + value: "<p>Some text</p><!-- 1 --><!-- 2 --><p>Some more text</p>", + result: "<p>Some text</p><!-- 1 --><!-- 2 --><p>Some more text</p>", + message: "HTML with comments; comments", + }, + { + config_input: { comments: false }, + value: "<p>Some text</p><!-- 1 --><!-- 2 --><p>Some more text</p>", + result: "<p>Some text</p><p>Some more text</p>", + message: "HTML with comments; !comments", + }, + { + config_input: {}, + value: "<p>comment<!-- hello -->in<!-- </p> -->text</p>", + result: "<p>commentintext</p>", + message: "HTML with comments deeper in the tree", + }, + { + config_input: { comments: true }, + value: "<p>comment<!-- hello -->in<!-- </p> -->text</p>", + result: "<p>comment<!-- hello -->in<!-- </p> -->text</p>", + message: "HTML with comments deeper in the tree, comments", + }, + { + config_input: { comments: false }, + value: "<p>comment<!-- hello -->in<!-- </p> -->text</p>", + result: "<p>commentintext</p>", + message: "HTML with comments deeper in the tree, !comments", + }, + { + config_input: { elements: ["svg"] }, + value: "<svg></svg>", + result: "", + message: + "Unknown HTML names (HTMLUnknownElement instances) should not match elements parsed as non-HTML namespaces.", + }, + { + config_input: { elements: ["div", "svg"] }, + value: "<div><svg></svg></div>", + result: "<div></div>", + message: + "Unknown HTML names (HTMLUnknownElement instances) should not match elements parsed as non-HTML namespaces when nested.", + }, + + // Case normalization (actually: lack of) + { + config_input: { removeElements: ["I", "DL"] }, + value: "<div>balabala<dl>test</dl></div>", + result: "<div>balabala<dl>test</dl></div>", + message: 'removeElements list ["I", "DL"]}', + }, + { + config_input: { removeElements: ["i", "dl"] }, + value: "<div>balabala<dl>test</dl></div>", + result: "<div>balabala</div>", + message: 'removeElements list ["i", "dl"]}', + }, + { + config_input: { removeElements: ["i", "dl"] }, + value: "<DIV>balabala<DL>test</DL></DIV>", + result: "<div>balabala</div>", + message: 'removeElements list ["i", "dl"]} with uppercase HTML', + }, + { + config_input: { removeAttributes: ["ID"] }, + value: '<p id="test">Click.</p>', + result: '<p id="test">Click.</p>', + message: 'removeAttributes list ["ID"] with id attribute', + }, + { + config_input: { removeAttributes: ["ID"] }, + value: '<p ID="test">Click.</p>', + result: '<p id="test">Click.</p>', + message: 'removeAttributes list ["ID"] with ID attribute', + }, + { + config_input: { removeAttributes: ["id"] }, + value: '<p ID="test">Click.</p>', + result: "<p>Click.</p>", + message: 'removeAttributes list ["id"] with ID attribute', + }, + + // unknownMarkup for elements (with and without) + { + config_input: { removeElements: [123, "test", "i", "custom-element"] }, + value: + "<div>balabala<i>test</i></div><test>t</test><custom-element>custom-element</custom-element>", + result: "<div>balabala</div>", + message: "removeElements with unknown elements and without unknownMarkup", + }, + { + config_input: { + replaceWithChildrenElements: [123, "test", "i", "custom-element"], + }, + value: + "<div>balabala<i>test</i></div><test>t</test><custom-element>custom-element</custom-element>", + result: "<div>balabalatest</div>", + message: + "replaceWithChildrenElements with unknown elements and without unknownMarkup", + }, + { + config_input: { elements: ["p", "test"] }, + value: "<div>test<div>p</div>tt<p>div</p></div><test>test</test>", + result: "testptt<p>div</p>", + message: "elements with unknown elements and without unknownMarkup", + }, + { + config_input: { + removeElements: [123, "test", "i", "custom-element"], + unknownMarkup: true, + }, + value: + "<div>balabala<i>test</i></div><test>t</test><custom-element>custom-element</custom-element>", + result: "<div>balabala</div>", + message: "removeElements with unknown elements and with unknownMarkup", + }, + { + config_input: { + replaceWithChildrenElements: [123, "test", "i", "custom-element"], + unknownMarkup: true, + }, + value: + "<div>balabala<i>test</i></div><test>t</test><custom-element>custom-element</custom-element>", + result: "<div>balabalatest</div>t", + message: + "replaceWithChildrenElements with unknown elements and with unknownMarkup", + }, + { + config_input: { elements: ["p", "test"], unknownMarkup: true }, + value: "<div>test<div>p</div>tt<p>div</p><test>test</test></div>", + result: "testptt<p>div</p><test>test</test>", + message: "elements with unknown elements and with unknownMarkup", + }, + + // unknownMarkup for attributes (with and without) + { + config_input: { + attributes: ["hello", "world"], + }, + value: "<div hello='1' world='2'><b hello='3' world='4'>", + result: "<div><b></b></div>", + message: "attributes: unknown attributes and without unknownMarkup", + }, + { + config_input: { + attributes: ["hello", "world"], + unknownMarkup: true, + }, + value: "<div hello='1' world='2'><b hello='3' world='4'>", + result: '<div hello="1" world="2"><b hello="3" world="4"></b></div>', + message: "attributes: unknown attributes and with unknownMarkup", + }, + { + config_input: { + removeAttributes: ["hello", "world"], + }, + value: "<div hello='1' world='2'><b hello='3' world='4'>", + result: "<div><b></b></div>", + message: "removeAttributes: unknown attributes and without unknownMarkup", + }, + { + config_input: { + removeAttributes: ["hello", "world"], + unknownMarkup: true, + }, + value: "<div hello='1' world='2'><b hello='3' world='4'>", + result: "<div><b></b></div>", + message: "removeAttributes unknown attributes and with allowUnknownMarkup", + }, +]; |