From 7ee7889ae8f1473474254553ec3b3469fb0a935b Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 18 Jun 2021 11:45:01 +0200 Subject: when possible inject CSP as http(s) header using webRequest instead of adding a tag --- TODOS.org | 11 +++-- background/background.html | 2 +- background/main.js | 4 +- background/policy_injector.js | 93 +++++++++++++++++++++++++++++++++++++++++++ content/main.js | 54 ++++++++++++++++--------- 5 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 background/policy_injector.js diff --git a/TODOS.org b/TODOS.org index 50a96a8..f53efbe 100644 --- a/TODOS.org +++ b/TODOS.org @@ -24,7 +24,7 @@ TODO: - test with more browser forks (Abrowser, Parabola IceWeasel, LibreWolf) - also see if browsers based on pre-quantum FF support enough of WebExtensions for easy porting -- make sure page's own csp doesn't block our scripts +- make sure page's own csp in doesn't block our scripts - make blocking more torough -- CRUCIAL - mind the data: urls -- CRUCIAL - find out how and make it possible to whitelist non-https urls and @@ -39,8 +39,13 @@ TODO: - all solutions to modularize js code SUCK; come up with own simple DSL to manage imports/exports - perform never-ending refactoring of already-written code -- when redirecting to target, make it possible to smartly recognize - and remove previous added target +- also implement support for whitelisting of non-https urls +- validate data entered in settings +- stop always using the same script nonce on given https(s) site (this + improvement seems to be unachievable in case of other protocols) +- besides blocking scripts through csp, also block connections that needlessly + fetch those scripts +- make extension's all html files proper XHTML DONE: - make it possible to use wildcard urls in settings -- DONE 2021-05-14 diff --git a/background/background.html b/background/background.html index b3e8010..53a74e9 100644 --- a/background/background.html +++ b/background/background.html @@ -37,7 +37,7 @@ - + diff --git a/background/main.js b/background/main.js index 4af7aa0..2f35321 100644 --- a/background/main.js +++ b/background/main.js @@ -30,12 +30,12 @@ const get_storage = window.get_storage; const start_storage_server = window.start_storage_server; const start_page_actions_server = window.start_page_actions_server; - const start_policy_smuggler = window.start_policy_smuggler; + const start_policy_injector = window.start_policy_injector; const browser = window.browser; start_storage_server(); start_page_actions_server(); - start_policy_smuggler(); + start_policy_injector(); async function init_myext(install_details) { diff --git a/background/policy_injector.js b/background/policy_injector.js new file mode 100644 index 0000000..e2d6358 --- /dev/null +++ b/background/policy_injector.js @@ -0,0 +1,93 @@ +/** + * Myext injecting policy to page using webRequest + * + * Copyright (C) 2021 Wojtek Kosior + * + * This code is dual-licensed under: + * - Asshole license 1.0, + * - GPLv3 or (at your option) any later version + * + * "dual-licensed" means you can choose the license you prefer. + * + * This code is released under a permissive license because I disapprove of + * copyright and wouldn't be willing to sue a violator. Despite not putting + * this code under copyleft (which is also kind of copyright), I do not want + * it to be made proprietary. Hence, the permissive alternative to GPL is the + * Asshole license 1.0 that allows me to call you an asshole if you use it. + * This means you're legally ok regardless of how you utilize this code but if + * you make it into something nonfree, you're an asshole. + * + * You should have received a copy of both GPLv3 and Asshole license 1.0 + * together with this code. If not, please see: + * - https://www.gnu.org/licenses/gpl-3.0.en.html + * - https://koszko.org/asshole-license.txt + */ + +"use strict"; + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const get_storage = window.get_storage; + const browser = window.browser; + const is_chrome = window.is_chrome; + const gen_unique = window.gen_unique; + const url_item = window.url_item; + const get_query_best = window.get_query_best; + + var storage; + var query_best; + + let csp_header_names = { + "content-security-policy" : true, + "x-webkit-csp" : true, + "x-content-security-policy" : true + }; + + function is_noncsp_header(header) + { + return !csp_header_names[header.name.toLowerCase()]; + } + + function inject(details) + { + let url = url_item(details.url); + + let [pattern, settings] = query_best(url); + + if (settings !== undefined && settings.allow) { + console.log("allowing", url); + return {cancel : false}; + } + + let nonce = gen_unique(url).substring(1); + let headers = details.responseHeaders.filter(is_noncsp_header); + headers.push({ + name : "content-security-policy", + value : `script-src 'nonce-${nonce}'; script-src-elem 'nonce-${nonce}';` + }); + + console.log("modified headers", url, headers); + + return {responseHeaders: headers}; + } + + async function start() { + storage = await get_storage(); + query_best = await get_query_best(); + + let extra_opts = ["blocking", "responseHeaders"]; + if (is_chrome) + extra_opts.push("extraHeaders"); + + browser.webRequest.onHeadersReceived.addListener( + inject, + { + urls: [""], + types: ["main_frame", "sub_frame"] + }, + extra_opts + ); + } + + window.start_policy_injector = start; +})(); diff --git a/content/main.js b/content/main.js index 23f7f66..eb5d0ac 100644 --- a/content/main.js +++ b/content/main.js @@ -30,29 +30,45 @@ const url_item = window.url_item; const gen_unique = window.gen_unique; - var url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; - var match = url_re.exec(document.URL); - var base_url = match[1]; - var first_target = match[3]; - var second_target = match[4]; + /* + * Due to some technical limitations the chosen method of whitelisting sites + * is to smuggle whitelist indicator in page's url as a "magical" string + * after '#'. Right now this is not needed in HTTP(s) pages where native + * script blocking happens through CSP header injection but is needed for + * protocols like ftp:// and file://. + * + * The code that actually injects the magical string into ftp:// and file:// + * urls has not yet been added to the extension. + */ - // TODO: can be refactored *a little bit* with policy_smuggler.js let url = url_item(document.URL); let unique = gen_unique(url); - let nonce = unique.substring(1); - var block = true; - if (first_target !== undefined && - first_target === unique) { - block = false; - console.log(["allowing", document.URL]); - if (second_target !== undefined) - window.location.href = base_url + second_target; - else - history.replaceState(null, "", base_url); - } else { - console.log(["not allowing", document.URL]); + function needs_blocking() + { + if (url.startsWith("https://") || url.startsWith("http://")) + return false; + + let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; + let match = url_re.exec(document.URL); + let base_url = match[1]; + let first_target = match[3]; + let second_target = match[4]; + + if (first_target !== undefined && + first_target === unique) { + if (second_target !== undefined) + window.location.href = base_url + second_target; + else + history.replaceState(null, "", base_url); + + console.log(["allowing whitelisted", document.URL]); + return false; + } + + console.log(["disallowing", document.URL]); + return true; } function handle_mutation(mutations, observer) @@ -129,7 +145,7 @@ script-src-elem 'nonce-${nonce}';\ } } - if (block) { + if (needs_blocking()) { var observer = new MutationObserver(handle_mutation); observer.observe(document.documentElement, { attributes: true, -- cgit v1.2.3