From 7bedbcbd80eba9359d2e905b7693923c76ce563d Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 17 Jan 2022 11:20:52 +0100 Subject: move policy enforcing code to a new file, include basic test --- content/policy_enforcing.js | 326 ++++++++++++++++++++++++++++++ copyright | 8 +- test/data/pages/gotmyowndomain.html | 2 +- test/data/pages/gotmyowndomain_https.html | 4 +- test/data/pages/scripts_to_block_1.html | 44 ++++ test/unit/test_policy_enforcing.py | 110 ++++++++++ test/unit/test_webrequest.py | 14 +- test/unit/utils.py | 13 ++ test/world_wide_library.py | 3 + 9 files changed, 504 insertions(+), 20 deletions(-) create mode 100644 content/policy_enforcing.js create mode 100644 test/data/pages/scripts_to_block_1.html create mode 100644 test/unit/test_policy_enforcing.py diff --git a/content/policy_enforcing.js b/content/policy_enforcing.js new file mode 100644 index 0000000..25c8b6b --- /dev/null +++ b/content/policy_enforcing.js @@ -0,0 +1,326 @@ +/** + * This file is part of Haketilo. + * + * Function: Enforcing script blocking rules on a given page, working from a + * content script. + * + * Copyright (C) 2021,2022 Wojtek Kosior + * Copyright (C) 2021 jahoti + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute forms of that code without the copy of the GNU + * GPL normally required by section 4, provided you include this + * license notice and, in case of non-source distribution, a URL + * through which recipients can access the Corresponding Source. + * If you modify file(s) with this exception, you may extend this + * exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * As a special exception to the GPL, any HTML file which merely + * makes function calls to this code, and for that purpose + * includes it by reference shall be deemed a separate work for + * copyright law purposes. If you modify this code, you may extend + * this exception to your version of the code, but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * I, Wojtek Kosior, thereby promise not to sue for violation of this file's + * license. Although I request that you do not make use of this code in a + * proprietary program, I am not going to enforce this in court. + */ + +#FROM common/misc.js IMPORT gen_nonce + +document.content_loaded = document.readyState === "complete"; +const wait_loaded = e => e.content_loaded ? Promise.resolve() : + new Promise(c => e.addEventListener("DOMContentLoaded", c, {once: true})); + +wait_loaded(document).then(() => document.content_loaded = true); + +/* + * In the case of HTML documents: + * 1. When injecting some payload we need to sanitize CSP tags before + * they reach the document. + * 2. Only tags inside are considered valid by the browser and + * need to be considered. + * 3. We want to detach from document, wait until its completes + * loading, sanitize it and re-attach . + * 4. We shall wait for anything to appear in or after and take that as + * a sign has finished loading. + * 5. Otherwise, getting the `DOMContentLoaded' event on the document shall also + * be a sign that is fully loaded. + */ + +function make_body_start_observer(DOM_element, waiting) { + const observer = new MutationObserver(() => try_body_started(waiting)); + observer.observe(DOM_element, {childList: true}); + return observer; +} + +function try_body_started(waiting) { + const body = waiting.detached_html.querySelector("body"); + + if ((body && (body.firstChild || body.nextSibling)) || + waiting.doc.documentElement.nextSibling) { + finish_waiting(waiting); + return true; + } + + if (body && waiting.observers.length < 2) + waiting.observers.push(make_body_start_observer(body, waiting)); +} + +function finish_waiting(waiting) { + if (waiting.finished) + return; + waiting.finished = true; + waiting.observers.forEach(observer => observer.disconnect()); + setTimeout(waiting.callback, 0); +} + +function _wait_for_head(doc, detached_html, callback) { + const waiting = {doc, detached_html, callback, observers: []}; + + if (try_body_started(waiting)) + return; + + waiting.observers = [make_body_start_observer(detached_html, waiting)]; + + wait_loaded(doc).then(() => finish_waiting(waiting)); +} + +function wait_for_head(doc, detached_html) { + return new Promise(cb => _wait_for_head(doc, detached_html, cb)); +} + +const blocked_str = "blocked"; + +function block_attribute(node, attr, ns=null, replace_with="") { + 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)); + /* + * Disabling attributes by prepending `blocked-' allows them to still be + * relatively easily accessed in case they contain some useful data. + */ + const construct_name = [attr]; + while (hasa(node, construct_name.join(""))) + construct_name.unshift(blocked_str); + + while (construct_name.length > 1) { + construct_name.shift(); + const name = construct_name.join(""); + seta(node, `${blocked_str}-${name}`, geta(node, name)); + } + + rema(node, attr); + seta(node, attr, replace_with); +} + +/* + * Used to disable ` + + + + + + + Click Meee! + + + + + + diff --git a/test/unit/test_policy_enforcing.py b/test/unit/test_policy_enforcing.py new file mode 100644 index 0000000..2f7bc80 --- /dev/null +++ b/test/unit/test_policy_enforcing.py @@ -0,0 +1,110 @@ +# SPDX-License-Identifier: CC0-1.0 + +""" +Haketilo unit tests - enforcing script blocking policy from content script +""" + +# This file is part of Haketilo +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the CC0 1.0 Universal License as published by +# the Creative Commons Corporation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# CC0 1.0 Universal License for more details. + +import pytest +import json +import urllib.parse +from selenium.webdriver.support.ui import WebDriverWait + +from ..script_loader import load_script +from .utils import are_scripts_allowed + +# For simplicity, we'll use one nonce in all test cases. +nonce = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + +allow_policy = {'allow': True} +block_policy = { + 'allow': False, + 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'none'; script-src-elem 'none'; frame-src http://* https://*;" +} +payload_policy = { + 'mapping': 'somemapping', + 'payload': {'identifier': 'someresource'}, + 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-{nonce}'; script-src-elem 'nonce-{nonce}';" +} + +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} : + 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(); + 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); +}''' + +def get(driver, page, what_to_do): + driver.get(page + '#' + urllib.parse.quote(json.dumps(what_to_do))) + driver.execute_script('window.before_reload = true; location.reload();') + done = lambda _: not driver.execute_script('return window.before_reload;') + WebDriverWait(driver, 10).until(done) + +@pytest.mark.ext_data({'content_script': content_script}) +@pytest.mark.usefixtures('webextension') +def test_policy_enforcing(driver, execute_in_page): + """ + A test case of sanitizing