aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content/policy_enforcing.js62
-rw-r--r--test/unit/test_policy_enforcing.py38
-rw-r--r--test/unit/utils.py6
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...
</body>
</html>`;
- 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 `<meta>'
@@ -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
+# <meta> 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 <script>s and <meta>s in pages.
+ A test case of sanitizing <script>s and intrinsic javascript in pages.
"""
# First, see if scripts run when not blocked.
get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': allow_policy
+ 'policy': allow_policy,
+ **csp_off_setting
})
for i in range(1, 3):
@@ -85,26 +86,29 @@ def test_policy_enforcing(driver, execute_in_page):
assert set(driver.execute_script('return window.__run || [];')) == \
{'inline', 'on', 'href', 'src', 'data'}
+ assert are_scripts_allowed(driver)
# Now, verify scripts don't run when blocked.
get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': block_policy
+ 'policy': block_policy,
+ **csp_off_setting
})
for i in range(1, 3):
driver.find_element_by_id(f'clickme{i}').click()
assert set(driver.execute_script('return window.__run || [];')) == set()
- assert not are_scripts_allowed(driver)
+ assert bool(csp_off_setting) == are_scripts_allowed(driver)
# Now, verify only scripts with nonce can run when payload is injected.
get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': payload_policy
+ 'policy': payload_policy,
+ **csp_off_setting
})
for i in range(1, 3):
driver.find_element_by_id(f'clickme{i}').click()
assert set(driver.execute_script('return window.__run || [];')) == set()
- assert not are_scripts_allowed(driver)
+ assert bool(csp_off_setting) == are_scripts_allowed(driver)
assert are_scripts_allowed(driver, nonce)
diff --git a/test/unit/utils.py b/test/unit/utils.py
index 8e04d91..4d8766e 100644
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -191,12 +191,12 @@ broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
def are_scripts_allowed(driver, nonce=None):
return driver.execute_script(
'''
- document.scripts_allowed = false;
+ document.haketilo_scripts_allowed = false;
const script = document.createElement("script");
- script.innerHTML = "document.scripts_allowed = true;";
+ script.innerHTML = "document.haketilo_scripts_allowed = true;";
if (arguments[0])
script.setAttribute("nonce", arguments[0]);
document.head.append(script);
- return document.scripts_allowed;
+ return document.haketilo_scripts_allowed;
''',
nonce)