Skip to content

Commit b13461b

Browse files
mfreed7chromium-wpt-export-bot
authored andcommitted
Change declarative Shadow DOM fragment parsing to be opt-in
This CL implements most of the suggestions from [1], which effectively block declarative Shadow DOM from being used by any fragment parser entry point, unless an explicit opt-in is toggled. The opt-ins include: - DOMParser.allowDeclarativeShadowDom = true; - HTMLTemplateElement.allowDeclarativeShadowDom = true; - XMLHttpRequest.allowDeclarativeShadowDom = true; - DocumentFragment.allowDeclarativeShadowDom = true; - Document.allowDeclarativeShadowDom = true; // For innerHTML - A new <iframe> sandbox flag: allow-declarative-shadow-dom This mitigates the potential client-side XSS sanitizer bypass detailed in the explainer and at [1]. Assuming these changes are functional, and mitigate the issue, this new behavior will be folded into the spec PRs at [2] and [3]. But given the security implications of the existing code, I'd like to get this landed first. [1] whatwg/dom#912 (comment) [2] whatwg/html#5465 [3] whatwg/dom#892 Bug: 1042130 Change-Id: I088f28f63078a0d26e354a4442494c0132b47ffc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2513525 Commit-Queue: Mason Freed <masonfreed@chromium.org> Reviewed-by: Kouhei Ueno <kouhei@chromium.org> Reviewed-by: Kinuko Yasuda <kinuko@chromium.org> Cr-Commit-Position: refs/heads/master@{#824591}
1 parent 3a36f4a commit b13461b

File tree

4 files changed

+259
-2
lines changed

4 files changed

+259
-2
lines changed

‎shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<script src='../resources/shadow-dom-utils.js'></script>
88

99
<script>
10+
document.allowDeclarativeShadowDom = true;
11+
1012
const shadowContent = '<span>Shadow tree</span><slot></slot>';
1113
function getDeclarativeContent(mode, delegatesFocus) {
1214
const delegatesFocusText = delegatesFocus ? ' shadowrootdelegatesfocus' : '';

‎shadow-dom/declarative/declarative-shadow-dom-basic.tentative.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
<script src="/resources/testharness.js"></script>
77
<script src="/resources/testharnessreport.js"></script>
88

9-
<div id="host">
9+
<div id="host" style="display:none">
1010
<template shadowroot="open">
1111
<slot id="s1" name="slot1"></slot>
1212
</template>
1313
<div id="c1" slot="slot1"></div>
1414
</div>
1515

1616
<script>
17+
document.allowDeclarativeShadowDom = true;
18+
1719
test(() => {
1820
const host = document.querySelector('#host');
1921
const c1 = host.querySelector('#c1');
@@ -129,7 +131,7 @@
129131
}, 'Declarative Shadow DOM: innerHTML root element');
130132
</script>
131133

132-
<div id="multi-host">
134+
<div id="multi-host" style="display:none">
133135
<template shadowroot="open">
134136
<span>root 1</span>
135137
</template>
@@ -157,6 +159,7 @@
157159
test(() => {
158160
const template = document.querySelector('#template-containing-shadow');
159161
const host1 = document.createElement('div');
162+
host1.style.display = 'none';
160163
document.body.appendChild(host1);
161164
host1.appendChild(template.content.cloneNode(true));
162165
let innerDiv = host1.querySelector('div.innerdiv');
@@ -165,6 +168,7 @@
165168
assert_equals(innerDiv.querySelector('template'), null, "No leftover template node");
166169

167170
const host2 = document.createElement('div');
171+
host2.style.display = 'none';
168172
document.body.appendChild(host2);
169173
host2.appendChild(template.content.cloneNode(true));
170174
innerDiv = host2.querySelector('div.innerdiv');
@@ -187,6 +191,7 @@
187191
test(() => {
188192
const template = document.querySelector('#template-containing-ua-shadow');
189193
const host = document.createElement('div');
194+
host.style.display = 'none';
190195
document.body.appendChild(host);
191196
// Mostly make sure clone of template *does* clone the
192197
// shadow root, and doesn't crash on cloning the <video>.
@@ -210,6 +215,7 @@
210215
test(() => {
211216
const template = document.querySelector('#template-containing-ua-shadow-closed');
212217
const host = document.createElement('div');
218+
host.style.display = 'none';
213219
document.body.appendChild(host);
214220
host.appendChild(template.content.cloneNode(true));
215221
let innerDiv = host.querySelector('div.innerdiv');
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Shadow DOM</title>
4+
<link rel="author" title="Mason Freed" href="mailto:masonfreed@chromium.org">
5+
<link rel="help" href="https://github.com/whatwg/dom/issues/831">
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
9+
<body>
10+
<style>
11+
* { white-space: pre; }
12+
iframe { display:none; }
13+
</style>
14+
<div id=log></div>
15+
16+
<div id=mainpage style="display:none">
17+
<div class=wrapper>
18+
<div class=host>
19+
<template shadowroot=open>
20+
<span class=content>Content</span>
21+
</template>
22+
</div>
23+
</div>
24+
</div>
25+
26+
<script>
27+
const content = `
28+
<html><body>
29+
<div class=wrapper>
30+
<div class=host>
31+
<template shadowroot=open>
32+
<span class=content>Content</span>
33+
</template>
34+
</div>
35+
</div>
36+
</body></html>
37+
`;
38+
39+
function assert_dsd(el,shouldHaveShadow) {
40+
const wrapper = el.querySelector('.wrapper');
41+
assert_true(!!wrapper,'Unable to find wrapper element');
42+
const host = wrapper.querySelector('.host');
43+
assert_true(!!host,'Unable to find host element');
44+
if (shouldHaveShadow) {
45+
assert_true(!!host.shadowRoot, 'Shadow root NOT FOUND.');
46+
assert_true(!!host.shadowRoot.querySelector('.content'),'Unable to locate content');
47+
} else {
48+
assert_true(!host.shadowRoot, 'Shadow root FOUND - none should be present.');
49+
const tmpl = host.querySelector('template');
50+
assert_true(!!tmpl, 'The template should be left as a <template> element');
51+
assert_equals(tmpl.getAttribute('shadowroot'),'open','The shadowroot attribute should still be present');
52+
assert_true(!!tmpl.content.querySelector('.content'),'Unable to locate content');
53+
}
54+
}
55+
56+
test(() => {
57+
const div = document.getElementById('mainpage');
58+
assert_dsd(div,true);
59+
assert_false(document.allowDeclarativeShadowDom,'The default value for allowDeclarativeShadowDom should be false');
60+
}, 'Non-fragment parsing needs no opt-in');
61+
62+
test(() => {
63+
const div = document.createElement('div');
64+
div.innerHTML = content;
65+
assert_false(document.allowDeclarativeShadowDom,'The default value for allowDeclarativeShadowDom should be false');
66+
assert_dsd(div,false);
67+
document.allowDeclarativeShadowDom = true;
68+
div.innerHTML = content;
69+
assert_dsd(div,true);
70+
document.allowDeclarativeShadowDom = false; // Don't affect other tests
71+
}, 'innerHTML on element');
72+
73+
test(() => {
74+
const templateContent = `<template id=tmpl>${content}</template>`;
75+
assert_false(document.allowDeclarativeShadowDom,'Default allowDeclarativeShadowDom should be false');
76+
const div = document.createElement('div');
77+
div.innerHTML = templateContent;
78+
assert_dsd(div.querySelector('#tmpl').content,false);
79+
document.allowDeclarativeShadowDom = true;
80+
div.innerHTML = templateContent;
81+
assert_dsd(div.querySelector('#tmpl').content,true);
82+
// Make sure to set back to avoid affecting other tests
83+
document.allowDeclarativeShadowDom = false; // Don't affect other tests
84+
}, 'innerHTML on element, with template content');
85+
86+
test(() => {
87+
const temp = document.createElement('template');
88+
temp.innerHTML = content;
89+
assert_dsd(temp.content,false, 'innerHTML by default should not allow declarative shadow content');
90+
assert_false(document.allowDeclarativeShadowDom,'The default value for document.allowDeclarativeShadowDom should be false');
91+
assert_false(temp.content.allowDeclarativeShadowDom,'The default value for template fragment allowDeclarativeShadowDom should be false');
92+
temp.content.allowDeclarativeShadowDom = true;
93+
assert_false(document.allowDeclarativeShadowDom,'Setting template allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
94+
temp.innerHTML = content;
95+
assert_true(temp.content.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom should persist across innerHTML set');
96+
assert_dsd(temp.content,true, 'innerHTML should allow declarative shadow content if template.content.allowDeclarativeShadowDom is set');
97+
temp.content.allowDeclarativeShadowDom = false;
98+
document.allowDeclarativeShadowDom = true;
99+
temp.innerHTML = content;
100+
assert_false(temp.content.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom should persist across innerHTML set');
101+
assert_true(document.allowDeclarativeShadowDom,'document.allowDeclarativeShadowDom should still be set');
102+
assert_dsd(temp.content,true, 'innerHTML should allow declarative shadow content if document.allowDeclarativeShadowDom is set');
103+
document.allowDeclarativeShadowDom = false; // Don't affect other tests
104+
}, 'Setting template.innerHTML');
105+
106+
test(() => {
107+
const templateContent = `<template id=tmpl>${content}</template>`;
108+
const temp = document.createElement('template');
109+
temp.innerHTML = templateContent;
110+
assert_dsd(temp.content.querySelector('#tmpl').content,false);
111+
document.allowDeclarativeShadowDom = true;
112+
temp.innerHTML = templateContent;
113+
assert_dsd(temp.content.querySelector('#tmpl').content,true);
114+
document.allowDeclarativeShadowDom = false; // Don't affect other tests
115+
}, 'Setting template.innerHTML with nested template content');
116+
117+
test(() => {
118+
const parser = new DOMParser();
119+
let fragment = parser.parseFromString(content,'text/html');
120+
assert_dsd(fragment.body,false);
121+
assert_false(parser.allowDeclarativeShadowDom,'The default value for allowDeclarativeShadowDom should be false');
122+
parser.allowDeclarativeShadowDom = true;
123+
assert_false(document.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
124+
fragment = parser.parseFromString(content,'text/html');
125+
assert_dsd(fragment.body,true);
126+
}, 'DOMParser');
127+
128+
test(() => {
129+
const doc = document.implementation.createHTMLDocument("Document");
130+
doc.body.innerHTML = content;
131+
assert_dsd(doc.body,false);
132+
assert_false(doc.allowDeclarativeShadowDom,'The default value for allowDeclarativeShadowDom should be false');
133+
doc.allowDeclarativeShadowDom = true;
134+
assert_false(document.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
135+
doc.body.innerHTML = content;
136+
assert_dsd(doc.body,true);
137+
}, 'createHTMLDocument');
138+
139+
test(() => {
140+
const doc = document.implementation.createHTMLDocument("Document");
141+
let range = doc.createRange();
142+
range.selectNode(doc.body);
143+
let documentFragment = range.createContextualFragment(content);
144+
assert_dsd(documentFragment,false);
145+
doc.allowDeclarativeShadowDom = true;
146+
assert_false(document.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
147+
documentFragment = range.createContextualFragment(content);
148+
assert_dsd(documentFragment,true);
149+
}, 'createContextualFragment');
150+
151+
async_test((t) => {
152+
let client = new XMLHttpRequest();
153+
client.addEventListener('load', t.step_func_done(() => {
154+
assert_true(client.status == 200 && client.responseXML != null);
155+
assert_dsd(client.responseXML.body,false);
156+
t.done();
157+
}));
158+
client.open("GET", `data:text/html,${content}`);
159+
client.responseType = 'document';
160+
assert_false(client.allowDeclarativeShadowDom,'The default value for allowDeclarativeShadowDom should be false');
161+
client.send();
162+
}, 'XMLHttpRequest, disabled');
163+
164+
async_test((t) => {
165+
let client = new XMLHttpRequest();
166+
client.addEventListener('load', t.step_func_done(() => {
167+
assert_true(client.status == 200 && client.responseXML != null);
168+
assert_dsd(client.responseXML.body,true);
169+
t.done();
170+
}));
171+
client.open("GET", `data:text/html,${content}`);
172+
client.responseType = 'document';
173+
client.allowDeclarativeShadowDom = true;
174+
assert_false(document.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
175+
client.send();
176+
}, 'XMLHttpRequest, enabled');
177+
178+
async_test((t) => {
179+
const iframe = document.createElement('iframe');
180+
iframe.style.display = "none";
181+
iframe.sandbox = "allow-same-origin";
182+
document.body.appendChild(iframe);
183+
iframe.addEventListener('load', t.step_func_done(() => {
184+
assert_dsd(iframe.contentDocument.body,false);
185+
t.done();
186+
}));
187+
iframe.srcdoc = content;
188+
}, 'iframe, disabled');
189+
190+
async_test((t) => {
191+
const iframe = document.createElement('iframe');
192+
iframe.style.display = "none";
193+
iframe.sandbox = "allow-same-origin allow-declarative-shadow-dom";
194+
document.body.appendChild(iframe);
195+
iframe.addEventListener('load', t.step_func_done(() => {
196+
assert_dsd(iframe.contentDocument.body,true);
197+
t.done();
198+
}));
199+
iframe.allowDeclarativeShadowDom = true;
200+
assert_false(document.allowDeclarativeShadowDom,'Setting allowDeclarativeShadowDom shouldn\'t affect document.allowDeclarativeShadowDom');
201+
iframe.srcdoc = content;
202+
}, 'iframe, enabled');
203+
204+
function getHandler(t, name, shouldHaveShadow) {
205+
return (e) => {
206+
t.step(() => {
207+
if (e.data.name == name) {
208+
assert_false(e.data.error,e.data.msg);
209+
assert_true(e.data.hasShadow == shouldHaveShadow);
210+
t.done();
211+
}
212+
});
213+
};
214+
}
215+
async_test((t) => {
216+
window.addEventListener('message', getHandler(t, 'iframe_disallow', false));
217+
}, 'iframe without allow-declarative-shadow-dom sandbox flag disallows declarative Shadow DOM');
218+
219+
async_test((t) => {
220+
window.addEventListener('message', getHandler(t,'iframe_allow', true));
221+
}, 'iframe with allow-declarative-shadow-dom sandbox flag allows declarative Shadow DOM');
222+
223+
</script>
224+
225+
<iframe name="iframe_disallow" sandbox="allow-scripts" src="support/declarative-child-frame.html" ></iframe>
226+
<iframe name="iframe_allow" sandbox="allow-scripts allow-declarative-shadow-dom" src="support/declarative-child-frame.html"></iframe>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div id=iframe>
2+
<div id=wrapper>
3+
<div id=host>
4+
<template shadowroot=open>
5+
<span id=content>Content</span>
6+
</template>
7+
</div>
8+
</div>
9+
</div>
10+
11+
<script>
12+
function sendStatus(error, hasShadow, msg) {
13+
const name = window.name;
14+
parent.postMessage({ name, error, hasShadow, msg }, '*');
15+
}
16+
17+
window.addEventListener('load', () => {
18+
const host = document.querySelector('#host');
19+
if (!host)
20+
return sendStatus(true, false, 'Unable to find host element');
21+
return sendStatus(false, !!host.shadowRoot);
22+
});
23+
</script>

0 commit comments

Comments
 (0)