From 31cc63c2b429b768379e1b2ef7598242d0b36d18 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 17 Jan 2022 14:15:43 +0100 Subject: test script blocking with and without the CSP-based approach on --- content/policy_enforcing.js | 62 +++++++++++++++++++++++++++----------- test/unit/test_policy_enforcing.py | 38 ++++++++++++----------- test/unit/utils.py | 6 ++-- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/content/policy_enforcing.js b/content/policy_enforcing.js index 25c8b6b..8e26afb 100644 --- a/content/policy_enforcing.js +++ b/content/policy_enforcing.js @@ -109,7 +109,7 @@ function wait_for_head(doc, detached_html) { const blocked_str = "blocked"; -function block_attribute(node, attr, ns=null, replace_with="") { +function block_attribute(node, attr, ns=null, replace_with=null) { const [hasa, geta, seta, rema] = ["has", "get", "set", "remove"] .map(m => (n, ...args) => typeof ns === "string" ? n[`${m}AttributeNS`](ns, ...args) : n[`${m}Attribute`](...args)); @@ -128,7 +128,8 @@ function block_attribute(node, attr, ns=null, replace_with="") { } rema(node, attr); - seta(node, attr, replace_with); + if (replace_with !== null) + seta(node, attr, replace_with); } /* @@ -180,11 +181,40 @@ function sanitize_tree_urls(root) { .forEach(sanitize_element_urls); } -function start_urls_sanitizing(doc) { - sanitize_tree_urls(doc); +#IF MOZILLA +function sanitize_element_onevent(element) { + for (const attribute_node of (element.attributes || [])) { + const attr = attribute_node.localName, attr_lo = attr.toLowerCase();; + if (!/^on/.test(attr_lo) || !(attr_lo in element.wrappedJSObject)) + continue; + + /* + * Guard against redefined getter on DOM object property. This should + * not be an issue */ + if (Object.getOwnPropertyDescriptor(element.wrappedJSObject, attr)) { + console.error("Redefined property on a DOM object! The page might have bypassed our script blocking measures!"); + continue; + } + element.wrappedJSObject[attr] = null; + block_attribute(element, attr, attribute_node.namespaceURI, + "javascript:void('blocked');"); + } +} + +function sanitize_tree_onevent(root) { + root.querySelectorAll("*") + .forEach(sanitize_element_onevent); +} +#ENDIF + +function start_mo_sanitizing(doc) { if (!doc.content_loaded) { - const mutation_handler = - m => m.addedNodes.forEach(sanitize_element_urls); + function mutation_handler(mutation) { + mutation.addedNodes.forEach(sanitize_element_urls); +#IF MOZILLA + mutation.addedNodes.forEach(sanitize_element_onevent); +#ENDIF + } const mo = new MutationObserver(ms => ms.forEach(mutation_handler)); mo.observe(doc, {childList: true, subtree: true}); wait_loaded(doc).then(() => mo.disconnect()); @@ -225,13 +255,8 @@ async function sanitize_document(doc, policy) { doc.addEventListener(...listener_args); wait_loaded(doc).then(() => doc.removeEventListener(...listener_args)); - for (const elem of doc.querySelectorAll("*")) { - [...elem.attributes].map(attr => attr.localName) - .filter(attr => /^on/.test(attr) && elem.wrappedJSObject[attr]) - .forEach(attr => elem.wrappedJSObject[attr] = null); - } - sanitize_tree_urls(doc.documentElement); + sanitize_tree_onevent(doc.documentElement); #ENDIF /* @@ -251,7 +276,7 @@ async function sanitize_document(doc, policy) { Loading... `; - const html = + const temporary_html = new DOMParser().parseFromString(source, "text/html").documentElement; /* @@ -259,7 +284,7 @@ async function sanitize_document(doc, policy) { * and sanitized. */ const root = doc.documentElement; - root.replaceWith(html); + root.replaceWith(temporary_html); /* * When we don't inject payload, we neither block document's CSP `' @@ -272,12 +297,15 @@ async function sanitize_document(doc, policy) { .forEach(m => sanitize_meta(m, policy)); } - root.querySelectorAll("script").forEach(s => sanitize_script(s, policy)); sanitize_tree_urls(root); - html.replaceWith(root); + root.querySelectorAll("script").forEach(s => sanitize_script(s, policy)); + temporary_html.replaceWith(root); root.querySelectorAll("script").forEach(s => desanitize_script(s, policy)); +#IF MOZILLA + sanitize_tree_onevent(root); +#ENDIF - start_urls_sanitizing(doc); + start_mo_sanitizing(doc); } async function _disable_service_workers() { diff --git a/test/unit/test_policy_enforcing.py b/test/unit/test_policy_enforcing.py index 2f7bc80..4b7c173 100644 --- a/test/unit/test_policy_enforcing.py +++ b/test/unit/test_policy_enforcing.py @@ -41,25 +41,17 @@ payload_policy = { content_script = load_script('content/policy_enforcing.js') + ''';{ const smuggled_what_to_do = /^[^#]*#?(.*)$/.exec(document.URL)[1]; -const what_to_do = smuggled_what_to_do === "" ? {allow: true} : +const what_to_do = smuggled_what_to_do === "" ? {policy: {allow: true}} : JSON.parse(decodeURIComponent(smuggled_what_to_do)); if (what_to_do.csp_off) { const orig_DOMParser = window.DOMParser; window.DOMParser = function() { - parser = new orig_DOMParser(); + const parser = new orig_DOMParser(); this.parseFromString = () => parser.parseFromString('', 'text/html'); } } -if (what_to_do.onbeforescriptexecute_off) - prevent_script_execution = () => {}; - -if (what_to_do.sanitize_script_off) { - sanitize_script = () => {}; - desanitize_script = () => {}; -} - enforce_blocking(what_to_do.policy); }''' @@ -71,13 +63,22 @@ def get(driver, page, what_to_do): @pytest.mark.ext_data({'content_script': content_script}) @pytest.mark.usefixtures('webextension') -def test_policy_enforcing(driver, execute_in_page): +# Under Mozilla we use several mechanisms of script blocking. Some serve as +# fallbacks in case others break. CSP one of those mechanisms. Here we run the +# test once with CSP blocking on and once without it. This allows us to verify +# that the CSP-less blocking approaches by themselves also work. We don't do the +# reverse (CSP on and other mechanisms off) because CSP rules added through +# injection are not reliable enough - they do not always take effect +# immediately and there's nothing we can do to fix it. +@pytest.mark.parametrize('csp_off_setting', [{}, {'csp_off': True}]) +def test_policy_enforcing_html(driver, execute_in_page, csp_off_setting): """ - A test case of sanitizing