Skip to content

Commit

Permalink
[Trusted Types] Implement new <script> handling for Trusted Types.
Browse files Browse the repository at this point in the history
This follows the proposal at
w3c/trusted-types#236
and effectively reverts crrev.com/c/1547746. This replaces the
(arguably rather invasive) changes in node.cc and element.cc with
more elaborate logic in html_script_element.cc. (I.e., it pushes
complexity from the super-classes into a specific subclass, at the
expense of making the sub-class do more work.)

Bug: 1026549
Change-Id: I929e9e669db7f9e6b8de5a3d0d0df661f109b644
  • Loading branch information
otherdaniel authored and chromium-wpt-export-bot committed Dec 9, 2019
1 parent c7df60f commit e139011
Showing 1 changed file with 208 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,235 @@
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support/helper.sub.js"></script>
<meta http-equiv="Content-Security-Policy" content="trusted-types *">
</head>
<body>
<div id="container"></div>
<script id="script1">"hello world!";</script>
<script id="trigger"></script>
<script>
var container = document.querySelector("#container");
const policy_dict = {
createScript: s => (s.includes("fail") ? null : s.replace("default", "count")),
createHTML: s => s.replace(/2/g, "3"),
};
const policy = trustedTypes.createPolicy("policy", policy_dict);

// Regression tests for 'Bypass via insertAdjacentText', reported at
// https://github.com/WICG/trusted-types/issues/133

// We are trying to assert that scripts do _not_ get executed. We
// accomplish by having the script under examination containing a
// postMessage, and to send a second guaranteed-to-execute postMessage
// so there's a point in time when we're sure the first postMessage
// must have arrived (if indeed it had been sent).
//
// We'll interpret the message data as follows:
// - includes "block": error (this message should have been blocked by TT)
// - includes "count": Count these, and later check against expect_count.
// - includes "done": Unregister the event handler and finish the test.
// - all else: Reject, as this is probably an error in the test.
function checkMessage(expect_count) {
postMessage("done", "*");
return new Promise((resolve, reject) => {
let count = 0;
globalThis.addEventListener("message", function handler(e) {
if (e.data.includes("block")) {
reject(`'block' received (${e.data})`);
} else if (e.data.includes("count")) {
count = count + 1;
} else if (e.data.includes("done")) {
globalThis.removeEventListener("message", handler);
if (expect_count && count != expect_count) {
reject(
`'done' received, but unexpected counts: expected ${expect_count} != actual ${count} (${e.data})`);
} else {
resolve(e.data);
}
} else {
reject("unexpected message received: " + e.data);
}
});
});
}

function checkSecurityPolicyViolationEvent(expect_count) {
return new Promise((resolve, reject) => {
let count = 0;
document.addEventListener("securitypolicyviolation", e => {
if (e.sample.includes("trigger")) {
if (expect_count && count != expect_count) {
reject(
`'trigger' received, but unexpected counts: expected ${expect_count} != actual ${count}`);
} else {
resolve();
}
} else {
count = count + 1;
}
});
try {
document.getElementById("trigger").text = "trigger fail";
} catch(e) { }
});
}

// Original report:
test(t => {
promise_test(t => {
// Setup: Create a <script> element with a <p> child.
let s = document.createElement("script");

// Sanity check: Element child content (<p>) doesn't count as source text.
let p = document.createElement("p");
p.textContent = "hello('world');";
p.text = "hello('world');";
s.appendChild(p);
container.appendChild(s);

// Sanity check: The <p> content doesn't count as source text.
assert_equals(s.text, "");

// Try to insertAdjacentText into the <script>, starting from the <p>
try {
p.insertAdjacentText("beforebegin", "hello('before');");
} catch (err) { }
assert_equals(s.text, "");
try {
p.insertAdjacentText("afterend", "hello('after');");
} catch (err) { }
assert_equals(s.text, "");
}, "Regression test: Bypass via insertAdjacentText, initial comment");
p.insertAdjacentText("beforebegin",
"postMessage('beforebegin should be blocked', '*');");
assert_true(s.text.includes("postMessage"));
p.insertAdjacentText("afterend",
"postMessage('afterend should be blocked', '*');");
assert_true(s.text.includes("after"));
return checkMessage();
}, "Regression test: Bypass via insertAdjacentText, initial comment.");

// Variant: Create a <script> element and create & insert a text node. Then
// insert it into the document container to make it live.
test(t => {
// Setup: Create a <script> element, and insert text via a text node.
let s = document.createElement("script");
let text = document.createTextNode("alert('hello');");
assert_throws(new TypeError(),
_ => { s.appendChild(text); },
"We should not be able to modify <script>.");
container.appendChild(s);
}, "Regression test: Bypass via appendChild into off-document script element");
const script_elements = {
"script": doc => doc.createElement("script"),
"svg:script": doc => doc.createElementNS("http://www.w3.org/2000/svg", "script"),
};
for (let [element, create_element] of Object.entries(script_elements)) {
// Like the "Original Report" test case above, but avoids use of the "text"
// accessor that <svg:script> doesn't support.
promise_test(t => {
let s = create_element(document);

// Variant: Create a <script> element and insert it into the document. Then
// create a text node and insert it into the live <script> element.
test(t => {
// Setup: Create a <script> element, insert it into the doc, and then create
// and insert text via a text node.
let s = document.createElement("script");
container.appendChild(s);
let text = document.createTextNode("alert('hello');");
assert_throws(new TypeError(),
_ => { s.appendChild(text); },
"We should not be able to modify <script>.");
}, "Regression test: Bypass via appendChild into live script element");
// Sanity check: Element child content (<p>) doesn't count as source text.
let p = document.createElement("p");
p.textContent = "hello('world');";
s.appendChild(p);
container.appendChild(s);

// Try to insertAdjacentText into the <script>, starting from the <p>
p.insertAdjacentText("beforebegin",
"postMessage('beforebegin should be blocked', '*');");
assert_true(s.textContent.includes("postMessage"));
p.insertAdjacentText("afterend",
"postMessage('afterend should be blocked', '*');");
assert_true(s.textContent.includes("after"));
return checkMessage();
}, "Regression test: Bypass via insertAdjacentText, initial comment. " + element);

// Variant: Create a <script> element and create & insert a text node. Then
// insert it into the document container to make it live.
promise_test(t => {
// Setup: Create a <script> element, and insert text via a text node.
let s = create_element(document);
let text = document.createTextNode("postMessage('appendChild with a " +
"text node should be blocked', '*');");
s.appendChild(text);
container.appendChild(s);
return checkMessage();
}, "Regression test: Bypass via appendChild into off-document script element" + element);

// Variant: Create a <script> element and insert it into the document. Then
// create a text node and insert it into the live <script> element.
promise_test(t => {
// Setup: Create a <script> element, insert it into the doc, and then create
// and insert text via a text node.
let s = create_element(document);
container.appendChild(s);
let text = document.createTextNode("postMessage('appendChild with a live " +
"text node should be blocked', '*');");
s.appendChild(text);
return checkMessage();
}, "Regression test: Bypass via appendChild into live script element " + element);
}

promise_test(t => {
return checkSecurityPolicyViolationEvent();
}, "Prep for subsequent tests: Clear SPV event queue.");

promise_test(t => {
const inserted_script = document.getElementById("script1");
assert_throws(new TypeError(), _ => {
inserted_script.innerHTML = "2+2";
});

let new_script = document.createElement("script");
assert_throws(new TypeError(), _ => {
new_script.innerHTML = "2+2";
container.appendChild(new_script);
});

const trusted_html = policy.createHTML("2*4");
assert_equals("" + trusted_html, "3*4");
inserted_script.innerHTML = trusted_html;
assert_equals(inserted_script.textContent, "3*4");

new_script = document.createElement("script");
new_script.innerHTML = trusted_html;
container.appendChild(new_script);
assert_equals(inserted_script.textContent, "3*4");

// We expect 3 SPV events: two for the two assert_throws cases, and one
// for script element, which will be rejected at the time of execution.
return checkSecurityPolicyViolationEvent(3);
}, "Spot tests around script + innerHTML interaction.");


// Create default policy. Wrapped in a promise_test to ensure it runs only
// after the other tests.
let default_policy;
promise_test(t => {
default_policy = trustedTypes.createPolicy("default", policy_dict);
return Promise.resolve();
}, "Prep for subsequent tests: Create default policy.");

for (let [element, create_element] of Object.entries(script_elements)) {
promise_test(t => {
let s = create_element(document);
let text = document.createTextNode("postMessage('default', '*');");
s.appendChild(text);
container.appendChild(s);
return Promise.all([checkMessage(1),
checkSecurityPolicyViolationEvent(0)]);
}, "Test that default policy applies. " + element);

promise_test(t => {
let s = create_element(document);
let text = document.createTextNode("fail");
s.appendChild(text);
container.appendChild(s);
return Promise.all([checkMessage(0),
checkSecurityPolicyViolationEvent(1)]);
}, "Test a failing default policy. " + element);
}

promise_test(t => {
const inserted_script = document.getElementById("script1");
inserted_script.innerHTML = "2+2";
assert_equals(inserted_script.textContent, "3+3");

let new_script = document.createElement("script");
new_script.innerHTML = "2+2";
container.appendChild(new_script);
assert_equals(inserted_script.textContent, "3+3");

const trusted_html = default_policy.createHTML("2*4");
assert_equals("" + trusted_html, "3*4");
inserted_script.innerHTML = trusted_html;
assert_equals(inserted_script.textContent, "3*4");

new_script = document.createElement("script");
new_script.innerHTML = trusted_html;
container.appendChild(new_script);
assert_equals(inserted_script.textContent, "3*4");

return checkSecurityPolicyViolationEvent(0);
}, "Spot tests around script + innerHTML interaction with default policy.");
</script>
</body>
</html>
Expand Down

0 comments on commit e139011

Please sign in to comment.