Skip to content

Commit

Permalink
[Sanitizer] Regression tests for "MVP" Sanitizer.
Browse files Browse the repository at this point in the history
The Sanitizer API spec is presently being heavily modified, and
the public WPT test suite is expected to follow. We make a copy of the
WPT test suite for the "MVP" launch of the Sanitizer, to act as regression
tests that ensure backwards compatibility.

Change-Id: Ic495554e6ece3f568d449b62630b1ab8d8cc85d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4239585
Reviewed-by: Yifan Luo <lyf@chromium.org>
Commit-Queue: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1106732}
  • Loading branch information
otherdaniel authored and Chromium LUCI CQ committed Feb 17, 2023
1 parent 98ef049 commit 5c7389f
Show file tree
Hide file tree
Showing 14 changed files with 695 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ monorail {
component: "Blink>SecurityFeature>SanitizerAPI"
}
team_email: "security-dev@chromium.org"
wpt {
notify: YES
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Sanitizer API MVP tests

This is a copy of external/wpt/sanitizer-api, in the last state that where
it passed for the initial Sanitizer "MVP" implementation. We put this copy
here in wpt_internal/sanitizer-api to ensure backward compatibility with
the launched Sanitizer state. I.e., as regression tests.

The tests in external/wpt/sanitizer-api will be modified as the external
Sanitizer spec evolves.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!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_true(node1 instanceof Node && node1.isEqualNode(node2),
`Node[${node1.innerHTML}] == Node[${node2.innerHTML}]`);
}

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>", new Sanitizer());
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, new Sanitizer());
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 => {
let s = new Sanitizer(testcase.config_input);
element.setHTML(testcase.value, {sanitizer: s });
assert_node_equals(buildNode(element.localName, testcase.result), element);
}, "Sanitizer: Element.setHTML with config: " + testcase.message);
}

[
undefined,
{},
{ sanitizer: new Sanitizer() },
{ sanitizer: undefined },
{ avocado: new Sanitizer() },
].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: null },
{ sanitizer: "avocado" },
{ sanitizer: { allowElements: [ "a", "b", "c" ] } },
].forEach((options, index) => {
test(t => {
assert_throws_js(TypeError, _ => {
document.createElement("div").setHTML("bla", options);
});
}, `Sanitizer: Element.setHTML invalid options dictionary #${index}`);
});

test(t => {
const sanitizer = new Sanitizer({allowElements: ["b"]});
const element = document.createElement("div");

// WebIDL magic: An IDL dictionary is mapped to a JS object. Thus, a plain
// Sanitizer instance will be accepted as an options dictionary. However,
// it will then try to read the .sanitizer property of the Sanitizer, and
// since that doesn't usually exist will treat it as an empty dictionary.
//
// Ref: https://webidl.spec.whatwg.org/#es-dictionary

// Sanitizer instance in the dictionary: Config is applied.
element.setHTML("<em>celery</em>", {sanitizer: sanitizer});
assert_equals(element.innerHTML, "celery");

// Same Sanitizer instance, passed directly: Is like an empty dictionary
// and config is not applied.
element.setHTML("<em>celery</em>", sanitizer);
assert_equals(element.innerHTML, "<em>celery</em>");

// Sanitizer-ception: Set the Sanitizer as the .sanitizer property on itself.
// Now the config is applied. It's magic. Just not the good kind of magic.
sanitizer.sanitizer = sanitizer;
element.setHTML("<em>celery</em>", sanitizer);
assert_equals(element.innerHTML, "celery");
}, "Sanitizer: Element.setHTML with sanitizer instance.");
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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({})']
});
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!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 => {
const div = document.createElement("div");
div.setHTML(probe_string, {sanitizer: sanitizer});
return probe_string == div.innerHTML;
};

const should_stay_the_same = {
allowElements: [ "div", "p" ],
blockElements: [ "test" ],
dropElements: [ "test" ],
allowAttributes: { "id": ["*"]},
dropAttributes: { "bla": ["blubb"]},
};
const should_modify = {
allowElements: [ "div", "span" ],
blockElements: [ "div" ],
dropElements: [ "p" ],
allowAttributes: { "id": ["p"]},
dropAttributes: { "id": ["div"]},
};

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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!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", "svg", "potato",

// Arguments will be stringified, so anything that stringifies to a valid
// name is also valid. (E.g. null => "null")
null, undefined, 123
];
const elems_invalid = [
"", "svg svg", "svg:svg", "potato:svg", [], ["*"], ["p"]
];

// Attribute names:
const attrs_valid = [
"href", "span"
];
const attrs_invalid = [
"svg:href", "svg href", "xlink:span", "xlink:href"
];

const all_elems = elems_valid.concat(elems_invalid);
const all_attrs = attrs_valid.concat(attrs_invalid);
for (const item of ["allowElements", "dropElements", "blockElements"]) {
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 ["allowAttributes", "dropAttributes"]) {
test(t => {
const sanitizer = new Sanitizer(
{[item]: Object.fromEntries(all_attrs.map(x => [x, ["*"]]))});
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.)
[
[ "p", "<p>Hello</p>" ],
[ "svg", "<svg>Hello</svg>", "" ],
[ "svg:svg", "<svg>Hello</svg>", "" ],
[ "math", "<math>Hello</math>", "" ],
[ "svg:math", "<math>Hello</math>", "" ],
[ "math:math", "<math>Hello</math>", "" ],
[ "potato:math", "<math>Hello</math>", "" ],
[ "potato:math", "<potato:math>Hello</potato:math>", "" ],
].forEach(([elem, probe, expected], index) => {
test(t => {
const sanitizer = new Sanitizer({allowElements: [elem]});
assert_equals(sanitizer.sanitizeFor("template", probe).innerHTML,
expected ?? probe);
}, `Namespaced elements #${index}: allowElements: ["${elem}"]`);
});

// Same for attributes:
[
[ "style", "<p style=\"bla\"></p>" ],
[ "href", "<p href=\"bla\"></p>" ],
[ "xlink:href", "<p xlink:href=\"bla\"></p>", "<p></p>" ],
[ "potato:href", "<p potato:href='bla'></p>", "<p></p>" ],
[ "xlink:href", "<p href='bla'></p>", "<p></p>" ],
[ "href", "<p xlink:href='bla'></p>", "<p></p>" ],
].forEach(([attr, probe, expected], index) => {
test(t => {
const sanitizer = new Sanitizer({allowAttributes: {[attr]: ["*"]}});
assert_equals(sanitizer.sanitizeFor("template", probe).innerHTML,
expected ?? probe);
}, `Namespaced attributes #${index}: allowAttributes: {"${attr}": ["*"]}`);
});
</script>
</body>
</html>
Loading

0 comments on commit 5c7389f

Please sign in to comment.