| <!DOCTYPE html> |
| <html> |
| <head> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';"> |
| </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: |
| 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.text = "hello('world');"; |
| s.appendChild(p); |
| container.appendChild(s); |
| assert_equals(s.text, ""); |
| |
| // Try to insertAdjacentText into the <script>, starting from the <p> |
| 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."); |
| |
| 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); |
| |
| // 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_js(TypeError, _ => { |
| inserted_script.innerHTML = "2+2"; |
| }); |
| |
| let new_script = document.createElement("script"); |
| assert_throws_js(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_js 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> |
| |