Skip to content

Commit fefb5fc

Browse files
noamrchromium-wpt-export-bot
authored andcommitted
Block CE reactions from elements disallowed by sanitizer
This applies both to regular fragment parsing and to streaming. We check the sanitizer early (when element is created) and avoid initializing the CE definition if the element is anyway going to be removed by the sanitizer. Bug: 538197156 Change-Id: Ib10400043a54710060d84477c9b30513e4acc1d8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8158260 Reviewed-by: Daniel Vogelheim <vogelheim@chromium.org> Commit-Queue: Noam Rosenthal <nrosenthal@google.com> Cr-Commit-Position: refs/heads/main@{#1669408}
1 parent 2e15912 commit fefb5fc

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<!DOCTYPE HTML>
2+
<meta charset="utf-8">
3+
<title>HTML streaming and positional setting APIs block Custom Element constructors when sanitized</title>
4+
<link rel="help" href="https://github.com/WICG/sanitizer-api" />
5+
<script src="/resources/testharness.js"></script>
6+
<script src="/resources/testharnessreport.js"></script>
7+
<body>
8+
<div id="container"></div>
9+
<script>
10+
let constructor_called = false;
11+
let connected_called = false;
12+
13+
class DisallowedElement extends HTMLElement {
14+
constructor() {
15+
super();
16+
constructor_called = true;
17+
}
18+
connectedCallback() {
19+
connected_called = true;
20+
}
21+
}
22+
customElements.define("disallowed-element", DisallowedElement);
23+
24+
const registry = new CustomElementRegistry();
25+
registry.define("disallowed-element-scoped", class extends HTMLElement {
26+
constructor() {
27+
super();
28+
constructor_called = true;
29+
}
30+
connectedCallback() {
31+
connected_called = true;
32+
}
33+
});
34+
35+
const asyncMethods = [
36+
"streamHTML", "streamAppendHTML", "streamPrependHTML",
37+
"streamHTMLUnsafe", "streamAppendHTMLUnsafe", "streamPrependHTMLUnsafe"
38+
];
39+
40+
const syncMethods = [
41+
"appendHTML", "prependHTML",
42+
"appendHTMLUnsafe", "prependHTMLUnsafe"
43+
];
44+
45+
for (const method of asyncMethods) {
46+
for (const config of [
47+
{ name: "removeElements", options: { sanitizer: { removeElements: ["disallowed-element"] } } },
48+
{ name: "allowElements", options: { sanitizer: { elements: ["div"] } } }
49+
]) {
50+
promise_test(async (t) => {
51+
constructor_called = false;
52+
connected_called = false;
53+
const container = document.getElementById("container");
54+
t.add_cleanup(() => container.replaceChildren());
55+
56+
const writer = container[method](config.options).getWriter();
57+
await writer.write("<disallowed-element></disallowed-element>");
58+
await writer.close();
59+
60+
assert_equals(container.innerHTML, "");
61+
assert_false(constructor_called, "Constructor should not be called for stripped elements");
62+
assert_false(connected_called, "ConnectedCallback should not be called for stripped elements");
63+
}, `${method} blocks CE constructor when element is dropped by sanitizer (${config.name})`);
64+
65+
promise_test(async (t) => {
66+
constructor_called = false;
67+
connected_called = false;
68+
const container = document.getElementById("container");
69+
t.add_cleanup(() => container.replaceChildren());
70+
71+
const shadow = container.attachShadow({ mode: "open", registry });
72+
t.add_cleanup(() => {
73+
const newContainer = document.createElement("div");
74+
newContainer.id = "container";
75+
container.replaceWith(newContainer);
76+
});
77+
78+
// Need to use the shadow root for streaming to test scoped registry
79+
const options = config.name === "removeElements"
80+
? { sanitizer: { removeElements: ["disallowed-element-scoped"] } }
81+
: { sanitizer: { elements: ["div"] } };
82+
83+
const writer = shadow[method](options).getWriter();
84+
await writer.write("<disallowed-element-scoped></disallowed-element-scoped>");
85+
await writer.close();
86+
87+
assert_equals(shadow.innerHTML, "");
88+
assert_false(constructor_called, "Constructor should not be called for stripped scoped elements");
89+
assert_false(connected_called, "ConnectedCallback should not be called for stripped scoped elements");
90+
}, `${method} blocks CE constructor in scoped registry when element is dropped by sanitizer (${config.name})`);
91+
}
92+
}
93+
94+
for (const method of syncMethods) {
95+
for (const config of [
96+
{ name: "removeElements", options: { sanitizer: { removeElements: ["disallowed-element"] } } },
97+
{ name: "allowElements", options: { sanitizer: { elements: ["div"] } } }
98+
]) {
99+
test((t) => {
100+
constructor_called = false;
101+
connected_called = false;
102+
const container = document.getElementById("container");
103+
t.add_cleanup(() => container.replaceChildren());
104+
105+
container[method]("<disallowed-element></disallowed-element>", config.options);
106+
107+
assert_equals(container.innerHTML, "");
108+
assert_false(constructor_called, "Constructor should not be called for stripped elements");
109+
assert_false(connected_called, "ConnectedCallback should not be called for stripped elements");
110+
}, `${method} blocks CE constructor when element is dropped by sanitizer (${config.name})`);
111+
112+
test((t) => {
113+
constructor_called = false;
114+
connected_called = false;
115+
const container = document.getElementById("container");
116+
t.add_cleanup(() => container.replaceChildren());
117+
118+
const shadow = container.attachShadow({ mode: "open", registry });
119+
t.add_cleanup(() => {
120+
const newContainer = document.createElement("div");
121+
newContainer.id = "container";
122+
container.replaceWith(newContainer);
123+
});
124+
125+
const options = config.name === "removeElements"
126+
? { sanitizer: { removeElements: ["disallowed-element-scoped"] } }
127+
: { sanitizer: { elements: ["div"] } };
128+
129+
shadow[method]("<disallowed-element-scoped></disallowed-element-scoped>", options);
130+
131+
assert_equals(shadow.innerHTML, "");
132+
assert_false(constructor_called, "Constructor should not be called for stripped scoped elements");
133+
assert_false(connected_called, "ConnectedCallback should not be called for stripped scoped elements");
134+
}, `${method} blocks CE constructor in scoped registry when element is dropped by sanitizer (${config.name})`);
135+
}
136+
}
137+
</script>
138+
</body>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
</head>
7+
<body>
8+
<div id="container"></div>
9+
<script>
10+
let constructor_called = false;
11+
let connectedCallback_called = false;
12+
13+
class DisallowedElement extends HTMLElement {
14+
constructor() {
15+
super();
16+
constructor_called = true;
17+
}
18+
connectedCallback() {
19+
connectedCallback_called = true;
20+
}
21+
}
22+
customElements.define("disallowed-element", DisallowedElement);
23+
24+
const registry = new CustomElementRegistry();
25+
registry.define("disallowed-element-scoped", class extends HTMLElement {
26+
constructor() {
27+
super();
28+
constructor_called = true;
29+
}
30+
connectedCallback() {
31+
connectedCallback_called = true;
32+
}
33+
});
34+
35+
function assert_removed(method, sanitizer, scoped = false) {
36+
constructor_called = connectedCallback_called = false;
37+
38+
let d = document.createElement("div");
39+
document.body.append(d);
40+
41+
let target = d;
42+
if (scoped) {
43+
target = d.attachShadow({ mode: "open", registry });
44+
}
45+
46+
const tag = scoped ? "disallowed-element-scoped" : "disallowed-element";
47+
48+
if (method === "setHTML") {
49+
target.setHTML(`<${tag}></${tag}>`, { sanitizer });
50+
} else if (method === "setHTMLUnsafe") {
51+
target.setHTMLUnsafe(`<${tag}></${tag}>`, { sanitizer });
52+
}
53+
54+
assert_equals(target.innerHTML, ``);
55+
assert_false(constructor_called, "Constructor should not be called");
56+
assert_false(connectedCallback_called, "ConnectedCallback should not be called");
57+
58+
d.remove();
59+
}
60+
61+
for (const method of ["setHTML", "setHTMLUnsafe"]) {
62+
for (const config of [
63+
{ name: "removeElements", options: { removeElements: ["disallowed-element"] }, scopedOptions: { removeElements: ["disallowed-element-scoped"] } },
64+
{ name: "allowElements", options: { elements: ["div"] }, scopedOptions: { elements: ["div"] } }
65+
]) {
66+
test(t => {
67+
assert_removed(method, config.options, false);
68+
}, `${method} blocks CE constructor when element is dropped by explicit sanitizer config (${config.name})`);
69+
70+
test(t => {
71+
assert_removed(method, config.scopedOptions, true);
72+
}, `${method} blocks CE constructor in scoped registry when element is dropped by explicit sanitizer config (${config.name})`);
73+
}
74+
}
75+
76+
</script>
77+
</body>
78+
</html>

0 commit comments

Comments
 (0)