From fea1317927128128f45748c0ba41b05d1975d545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Wang?= Date: Thu, 16 Jan 2025 16:16:42 +0100 Subject: [PATCH] Rewrite Trusted types tests for CSP violations Currently the listener to "securitypolicyviolation" is added before actually running the statement that triggers violations, so it could be possible that some violations are not caught. This bad pattern is duplicated in several `trusted-types*reporting*` tests. This patch adds a new helper file to properly wrap the listener registration and statement execution in a promise, and reuses it in existing tests. https://github.com/w3c/trusted-types/issues/576 --- trusted-types/support/csp-violations.js | 60 ++++ trusted-types/trusted-types-reporting.html | 320 +++++++++------------ 2 files changed, 194 insertions(+), 186 deletions(-) create mode 100644 trusted-types/support/csp-violations.js diff --git a/trusted-types/support/csp-violations.js b/trusted-types/support/csp-violations.js new file mode 100644 index 00000000000000..6cca35e89aeb66 --- /dev/null +++ b/trusted-types/support/csp-violations.js @@ -0,0 +1,60 @@ +const trustedTypeDirectives = [ + // https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive + "require-trusted-types-for", + // https://w3c.github.io/trusted-types/dist/spec/#trusted-types-csp-directive + "trusted-types", +]; + +// A generic helper that runs function fn and return a promise resolving with +// an array of reported violations for trusted type directives and a possible +// exception thrown. +function trusted_type_violations_and_exception_for(fn) { + return new Promise((resolve, reject) => { + // Listen for security policy violations. + let result = { violations: [], exception: null }; + let handler = e => { + if (trustedTypeDirectives.includes(e.effectiveDirective)) { + result.violations.push(e); + } else if (e.effectiveDirective === "object-src") { + document.removeEventListener("securitypolicyviolation", handler); + e.stopPropagation(); + resolve(result); + } else { + reject("Unexpected violation for directive ${e.effectiveDirective}"); + } + } + document.addEventListener("securitypolicyviolation", handler); + + // Run the specified function and record any exception. + try { + fn(); + } catch(e) { + result.exception = e; + } + + // Force an "object-src" violation, to make sure all the previous violations + // have been delivered. This assumes the test file's associated .headers + // file contains Content-Security-Policy: object-src 'none'. + var o = document.createElement('object'); + o.type = "video/mp4"; + o.data = "dummy.webm"; + document.body.appendChild(o); + }); +} + +// Helper function when we expect one violation and exception. +async function trusted_type_violation_for(expectedException, fn) { + let {violations, exception} = + await trusted_type_violations_and_exception_for(fn); + assert_equals(violations.length, 1, "a single violation reported"); + assert_true(exception instanceof expectedException, "TypeError exception reported"); + return violations[0]; +} + +// Helper function when we expect no violation or exception. +async function no_trusted_type_violation_report_for(fn) { + let {violations, exception} = + await trusted_type_violations_and_exception_for(fn); + assert_equals(violations.length, 0, "no violation reported"); + assert_equals(exception, null, "no exception thrown"); +} diff --git a/trusted-types/trusted-types-reporting.html b/trusted-types/trusted-types-reporting.html index 96c9dd72813a7b..6148123407920e 100644 --- a/trusted-types/trusted-types-reporting.html +++ b/trusted-types/trusted-types-reporting.html @@ -3,7 +3,7 @@ - + @@ -33,52 +33,11 @@ const url = "" + document.location; - // Return function that returns a promise that resolves on the given - // violation report. - // - // filter_arg - iff function, call it with the event object. - // Else, string-ify and compare against event.originalPolicy. - function promise_violation(filter_arg) { - return _ => new Promise((resolve, reject) => { - function handler(e) { - let matches = (filter_arg instanceof Function) - ? filter_arg(e) - : (e.originalPolicy.includes(filter_arg)); - if (matches) { - document.removeEventListener("securitypolicyviolation", handler); - e.stopPropagation(); - resolve(e); - } - } - document.addEventListener("securitypolicyviolation", handler); - }); - } - - // Like assert_throws_*, but we don't care about the exact error. We just want - // to run the code and continue. - function expect_throws(fn) { - try { fn(); } catch (err) { return; /* ignore */ } - assert_unreached(); - } - - // Test the "sample" field of the event. // TODO(vogelheim): The current set of tests allows for more variance in the // sample reports than the current spec draft does. Once the spec has // been finalized, we should clamp this down to check byte-for-byte // against the values mandated by the spec. - function expect_sample(s) { return e => { - assert_true(e.sample.includes(s), - `expected "${e.sample}" to include "${s}".`); - return e; - } } - - function expect_blocked_uri(s) { return e => { - assert_equals(e.blockedURI, s, - `expected "${e.blockedURI}" to be "${s}".`); - return e; - } } - // A sample policy we use to test trustedTypes.createPolicy behaviour. const id = x => x; const a_policy = { @@ -87,172 +46,163 @@ createScript: id, }; - // Provoke/wait for a CSP violation, in order to be sure that all previous - // CSP violations have been delivered. - function promise_flush() { - return promise_violation("object-src 'none'"); - } - function flush() { - expect_throws(_ => { - var o = document.createElement('object'); - o.type = "video/mp4"; - o.data = "dummy.webm"; - document.body.appendChild(o); - }); - } - - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("trusted-types one")) - .then(promise_violation("trusted-types two")) - .then(expect_sample("three")) - .then(expect_blocked_uri("trusted-types-policy")) - .then(promise_flush()); - expect_throws(_ => trustedTypes.createPolicy("three", a_policy)); - flush(); - return p; + promise_test(async t => { + let {violations, exception} = + await trusted_type_violations_and_exception_for(_ => + trustedTypes.createPolicy("three", a_policy) + ); + assert_equals(violations.length, 2); + assert_true(violations[0].originalPolicy.includes("trusted-types one")); + assert_true(violations[1].originalPolicy.includes("trusted-types two")); + assert_true(violations[1].sample.includes("three")); + assert_equals(violations[1].blockedURI, "trusted-types-policy"); + assert_true(exception instanceof TypeError); }, "Trusted Type violation report: creating a forbidden policy."); - promise_test(t => { - let p = promise_flush()(); - expect_throws(_ => trustedTypes.createPolicy("two", a_policy)); - flush(); - return p; + promise_test(async t => { + let {violations, exception} = + await trusted_type_violations_and_exception_for(_ => + trustedTypes.createPolicy("two", a_policy) + ); + assert_equals(violations.length, 1); + assert_true(violations[0].originalPolicy.includes("trusted-types one")); + assert_true(violations[0].sample.includes("two")); + assert_equals(violations[0].blockedURI, "trusted-types-policy"); + assert_true(exception instanceof TypeError); }, "Trusted Type violation report: creating a report-only-forbidden policy."); // policy_one is set below, and used in several tests further down. let policy_one = null; - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("trusted-types two")) - .then(promise_flush()); - policy_one = trustedTypes.createPolicy("one", a_policy); - flush(); - return p; + promise_test(async t => { + let {violations, exception} = + await trusted_type_violations_and_exception_for(_ => + policy_one = trustedTypes.createPolicy("one", a_policy) + ); + assert_equals(violations.length, 1); + assert_true(violations[0].originalPolicy.includes("trusted-types two")); + assert_true(violations[0].sample.includes("one")); + assert_equals(violations[0].blockedURI, "trusted-types-policy"); + assert_equals(exception, null); }, "Trusted Type violation report: creating a forbidden-but-not-reported policy."); - promise_test(t => { - let p = promise_violation("require-trusted-types-for 'script")() - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("Element insertAdjacentHTML|x")); - expect_throws(() => { - document.getElementById("div").insertAdjacentHTML("beforebegin", "x"); - }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("div").insertAdjacentHTML("beforebegin", "x") + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); }, "Trusted Type violation report: blocked URI and sample for insertAdjacentHTML"); - promise_test(t => { - let p = promise_violation("require-trusted-types-for 'script'")(); - expect_throws(_ => document.getElementById("script").src = url); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("script").src = url + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); }, "Trusted Type violation report: assign string to script url"); - promise_test(t => { - let p = promise_violation("require-trusted-types-for 'script'")(); - expect_throws(_ => document.getElementById("div").innerHTML = "abc"); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("div").innerHTML = "abc" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); }, "Trusted Type violation report: assign string to html"); - promise_test(t => { - let p = promise_flush()(); - document.getElementById("script").text = policy_one.createScript("2+2;"); - flush(); - return p; + promise_test(async t => { + await no_trusted_type_violation_report_for(_ => + document.getElementById("script").text = policy_one.createScript("2+2;") + ); }, "Trusted Type violation report: assign trusted script to script; no report"); - promise_test(t => { - let p = promise_flush()(); - document.getElementById("div").innerHTML = policy_one.createHTML("abc"); - flush(); - return p; + promise_test(async t => { + await no_trusted_type_violation_report_for(_ => + document.getElementById("div").innerHTML = policy_one.createHTML("abc") + ); }, "Trusted Type violation report: assign trusted HTML to html; no report"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("Element innerHTML|abc")); - expect_throws(_ => { document.getElementById("div").innerHTML = "abc" }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("div").innerHTML = "abc" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("Element innerHTML|abc")); }, "Trusted Type violation report: sample for innerHTML assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLScriptElement text|abc")); - expect_throws(_ => { document.getElementById("script").text = "abc" }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("script").text = "abc" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("HTMLScriptElement text|abc")); }, "Trusted Type violation report: sample for text assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLScriptElement src")); - expect_throws(_ => { document.getElementById("script").src = "" }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("script").src = "" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("HTMLScriptElement src")); }, "Trusted Type violation report: sample for script.src assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLScriptElement innerText|2+2;")); - expect_throws(_ => document.getElementById("script").innerText = "2+2;"); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("script").innerText = "2+2;" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("Element innerText|2+2")); }, "Trusted Type violation report: sample for script innerText assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("SVGScriptElement href")); - expect_throws(_ => { document.getElementById("svgscript").href.baseVal = "" }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("svgscript").href.baseVal = "" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("SVGScriptElement href")); }, "Trusted Type violation report: sample for SVGScriptElement href assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("SVGScriptElement href")); - expect_throws(_ => { document.getElementById("svgscript").setAttribute('href', "test"); }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("svgscript").setAttribute('href', "test") + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("SVGScriptElement href")); }, "Trusted Type violation report: sample for SVGScriptElement href assignment by setAttribute"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("SVGScriptElement text")); - expect_throws(_ => { document.getElementById("svgscript").insertBefore(document.createTextNode("Hello"), null) }); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("svgscript").insertBefore(document.createTextNode("Hello"), null) + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("SVGScriptElement text")); }, "Trusted Type violation report: sample for SVGScriptElement text assignment"); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("eval|2+2")) - .then(promise_flush()); - expect_throws(_ => eval("2+2")); - flush(); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(EvalError, _ => + eval("2+2") + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("eval|2+2")); }, "Trusted Type violation report: sample for eval"); - promise_test(t => { + promise_test(async t => { // We expect the sample string to always contain the name, and at least the // start of the value, but it should not be excessively long. - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLScriptElement innerText|abbb")) - .then(e => assert_less_than(e.sample.length, 150)); const value = "a" + "b".repeat(50000); - expect_throws(_ => document.getElementById("script").innerText = value); - return p; + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("script").innerText = value + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("HTMLScriptElement innerText|abbb")); + assert_less_than(violation.sample.length, 150); }, "Trusted Type violation report: large values should be handled sanely."); // Test reporting for Custom Elements (where supported). The report should @@ -262,25 +212,23 @@ class CustomScript extends HTMLScriptElement {}; customElements.define("custom-script", CustomScript, { extends: "script" }); - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLScriptElement src|abc")); - expect_throws(_ => document.getElementById("customscript").src = "abc"); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + document.getElementById("customscript").src = "abc" + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("HTMLScriptElement src|abc")); }, "Trusted Type violation report: sample for custom element assignment"); } - promise_test(t => { - let p = Promise.resolve() - .then(promise_violation("require-trusted-types-for 'script'")) - .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("Worker constructor|")) - .then(promise_flush()); - expect_throws(_ => new Worker("blabla")); - flush(); - return p; + promise_test(async t => { + let violation = await trusted_type_violation_for(TypeError, _ => + new Worker("blabla") + ); + assert_true(violation.originalPolicy.includes("require-trusted-types-for 'script'")); + assert_equals(violation.blockedURI, "trusted-types-sink"); + assert_true(violation.sample.includes("Worker constructor|")); }, "Trusted Type violation report: Worker constructor");