diff options
-rw-r--r-- | background/policy_injector.js | 65 | ||||
-rwxr-xr-x | build.sh | 2 | ||||
-rw-r--r-- | common/misc.js | 97 | ||||
-rw-r--r-- | content/freezer.js | 2 | ||||
-rw-r--r-- | content/main.js | 31 | ||||
-rw-r--r-- | content/page_actions.js | 1 | ||||
-rw-r--r-- | copyright | 3 | ||||
-rw-r--r-- | html/options_main.js | 13 |
8 files changed, 113 insertions, 101 deletions
diff --git a/background/policy_injector.js b/background/policy_injector.js index 2cd7b6e..ee97333 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -15,8 +15,9 @@ * IMPORT is_mozilla * IMPORT gen_unique * IMPORT gen_nonce + * IMPORT is_privileged_url * IMPORT url_item - * IMPORT url_extract_policy + * IMPORT url_extract_target * IMPORT sign_policy * IMPORT get_query_best * IMPORT csp_rule @@ -39,27 +40,24 @@ function is_csp_header(header) return !!csp_header_names[header.name.toLowerCase()]; } -function is_our_header(header, rule) -{ - return header.value === rule -} - function url_inject(details) { - const targets = url_extract_policy(details.url); - if (targets.current) { + if (is_privileged_url(details.url)) return; - } else if (targets.policy) { - /* Redirect; update policy */ - targets.target = targets.target2; - delete targets.target2 - } + + const targets = url_extract_target(details.url); + if (targets.current) + return; + + /* Redirect; update policy */ + if (targets.policy) + targets.target = ""; let [pattern, settings] = query_best(targets.base_url); + /* Defaults */ if (!pattern) - /* Defaults */ settings = {}; - + const policy = encodeURIComponent( JSON.stringify({ allow: settings.allow, @@ -67,39 +65,40 @@ function url_inject(details) base_url: targets.base_url }) ); - - let redirect_url = targets.base_url; - redirect_url += '#' + sign_policy(policy, new Date()) + policy; - if (targets.target) - redirect_url += targets.target; - if (targets.target2) - redirect_url += targets.target2; - - return {redirectUrl: redirect_url}; + + return { + redirectUrl: [ + targets.base_url, + '#', sign_policy(policy, new Date()), policy, + targets.target, + targets.target2 + ].join("") + }; } -function inject(details) +function headers_inject(details) { - const targets = url_extract_policy(details.url); + const targets = url_extract_target(details.url); + /* Block mis-/unsigned requests */ if (!targets.current) - /* Block mis-/unsigned requests */ return {cancel: true}; const rule = csp_rule(targets.policy.nonce); var headers = details.responseHeaders; + /* + * Chrome doesn't have the buggy behavior of caching headers + * we injected. Firefox does and we have to remove it there. + */ if (!targets.policy.allow || is_mozilla) - /* - * Chrome doesn't have the buggy behavior of caching headers - * we injected. Firefox does and we have to remove it there. - */ headers = headers.filter(h => !is_csp_header(h)); - if (!targets.policy.allow) + if (!targets.policy.allow) { headers.push({ name : header_name, value : rule }); + } return {responseHeaders: headers}; } @@ -123,7 +122,7 @@ async function start_policy_injector() ); browser.webRequest.onHeadersReceived.addListener( - inject, + headers_inject, { urls: ["<all_urls>"], types: ["main_frame", "sub_frame"] @@ -194,7 +194,7 @@ set_browser() { main() { set_browser "$1" - + # placate importers of these, as they are exported by the yet-to-be-created exports_init.js EXPORTS__browser=exports_init.js EXPORTS__is_chrome=exports_init.js diff --git a/common/misc.js b/common/misc.js index 472620e..8cb26ab 100644 --- a/common/misc.js +++ b/common/misc.js @@ -27,6 +27,23 @@ function gen_nonce() * generating unique, per-site value that can be computed synchronously * and is impossible to guess for a malicious website */ + +/* Uint8toHex is a separate function not exported as (a) it's useful and (b) it will be used in crypto.subtle-based digests */ +function Uint8toHex(data) +{ + let returnValue = ''; + for (let byte of data) + returnValue += ('00' + byte.toString(16)).slice(-2); + return returnValue; +} + +function gen_nonce(length) // Default 16 +{ + let randomData = new Uint8Array(length || 16); + crypto.getRandomValues(randomData); + return Uint8toHex(randomData); +} + function gen_unique(url) { return sha256(get_secure_salt() + url); @@ -52,24 +69,51 @@ function url_item(url) } /* - * Assume a url like: https://example.com/green?illuminati=confirmed#tinky#winky + * Assume a url like: + * https://example.com/green?illuminati=confirmed#<injected-policy>#winky * This function will make it into an object like: * { - * "base_url" : "https://example.com/green?illuminati=confirmed", - * "target" : "#tinky", - * "target2" : "#winky" + * "base_url": "https://example.com/green?illuminati=confirmed", + * "target": "#<injected-policy>", + * "target2": "#winky", + * "policy": <injected-policy-as-js-object>, + * "current": <boolean-indicating-whether-policy-url-matches> * } * In case url doesn't have 2 #'s, target2 and target can be set to undefined. */ function url_extract_target(url) { - let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; - let match = url_re.exec(url); - return { - base_url : match[1], - target : match[3], - target2 : match[4] + const url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; + const match = url_re.exec(url); + const targets = { + base_url: match[1], + target: match[3] || "", + target2: match[4] || "" }; + if (!targets.target) + return targets; + + /* %7B -> { */ + const index = targets.target.indexOf('%7B'); + if (index === -1) + return targets; + + const now = new Date(); + const sig = targets.target.substring(1, index); + const policy = targets.target.substring(index); + if (sig !== sign_policy(policy, now) && + sig !== sign_policy(policy, now, -1)) + return targets; + + try { + targets.policy = JSON.parse(decodeURIComponent(policy)); + targets.current = targets.policy.base_url === targets.base_url; + } catch (e) { + /* This should not be reached - it's our self-produced valid JSON. */ + console.log("Unexpected internal error - invalid JSON smuggled!", e); + } + + return targets; } /* csp rule that blocks all scripts except for those injected by us */ @@ -110,45 +154,12 @@ function sign_policy(policy, now, hours_offset) { return gen_unique(time + policy); } -/* Extract any policy present in the URL */ -function url_extract_policy(url) -{ - const targets = url_extract_target(url); - if (!targets.target) - return targets; - - /* %7B -> { */ - const index = targets.target.indexOf('%7B'); - if (index === -1) - return targets; - - const now = new Date(); - const sig = targets.target.substring(1, index); - const policy = targets.target.substring(index); - if ( - sig !== sign_policy(policy, now) && - sig !== sign_policy(policy, now, -1) && - sig !== sign_policy(policy, now, 1) - ) - return targets; - - try { - targets.policy = JSON.parse(decodeURIComponent(policy)); - targets.current = targets.policy.base_url === targets.base_url; - } catch (e) { - /* TODO what should happen here? */ - } - - return targets; -} - /* * EXPORTS_START * EXPORT gen_nonce * EXPORT gen_unique * EXPORT url_item * EXPORT url_extract_target - * EXPORT url_extract_policy * EXPORT sign_policy * EXPORT csp_rule * EXPORT nice_name diff --git a/content/freezer.js b/content/freezer.js index 8e543a6..9dbc95e 100644 --- a/content/freezer.js +++ b/content/freezer.js @@ -49,7 +49,7 @@ function mozilla_suppress_scripts(e) { console.log('Script suppressor has detached.'); return; } - else if (e.isTrusted) { // Prevent blocking of injected scripts + if (e.isTrusted && !e.target._hachette_payload) { e.preventDefault(); console.log('Suppressed script', e.target); } diff --git a/content/main.js b/content/main.js index a5e04fd..af8cd7c 100644 --- a/content/main.js +++ b/content/main.js @@ -11,7 +11,6 @@ * IMPORT handle_page_actions * IMPORT url_item * IMPORT url_extract_target - * IMPORT url_extract_policy * IMPORT gen_unique * IMPORT gen_nonce * IMPORT csp_rule @@ -27,14 +26,16 @@ /* * 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://. + * after '#'. Right now this is only supplemental in HTTP(s) pages where + * blocking of native scripts also happens through CSP header injection but is + * necessary 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. */ +var nonce = undefined; + function handle_mutation(mutations, observer) { if (document.readyState === 'complete') { @@ -58,9 +59,8 @@ function block_nodes_recursively(node) function block_node(node) { /* - * Modifying <script> element doesn't always prevent its - * execution in some Mozilla browsers. Additional blocking - * through CSP meta tag injection is required. + * Modifying <script> element doesn't always prevent its execution in some + * Mozilla browsers. This is Chromium-specific code. */ if (node.tagName === "SCRIPT") { block_script(node); @@ -99,21 +99,20 @@ function inject_csp(head) } if (!is_privileged_url(document.URL)) { - const targets = url_extract_policy(document.URL); + const targets = url_extract_target(document.URL); if (targets.policy) { - if (targets.target2 !== undefined) + if (targets.target2) window.location.href = targets.base_url + targets.target2; else history.replaceState(null, "", targets.base_url); } - - targets.policy = targets.current ? targets.policy : {}; - - const nonce = targets.policy.nonce || gen_nonce(); - start_activity_info_server(); + + const policy = targets.current ? targets.policy : {}; + + nonce = policy.nonce || gen_nonce(); handle_page_actions(nonce); - if (!targets.policy.allow) { + if (!policy.allow) { block_nodes_recursively(document.documentElement); if (is_chrome) { @@ -128,4 +127,6 @@ if (!is_privileged_url(document.URL)) { if (is_mozilla) addEventListener('beforescriptexecute', mozilla_suppress_scripts, true); } + + start_activity_info_server(); } diff --git a/content/page_actions.js b/content/page_actions.js index fd405fe..75cc4d9 100644 --- a/content/page_actions.js +++ b/content/page_actions.js @@ -50,6 +50,7 @@ function add_script(script_text) let script = document.createElement("script"); script.textContent = script_text; script.setAttribute("nonce", nonce); + script._hachette_payload = true; document.body.appendChild(script); report_script(script_text); @@ -8,9 +8,10 @@ License: GPL-3+-javascript or Alicense-1.0 Files: build.sh Copyright: 2021 Wojtek Kosior <koszko@koszko.org> + 2021 jahoti <jahoti@tilde.team> License: CC0 -Files: manifest.json background/policy_injector.js common/misc.js content/main.js +Files: background/policy_injector.js common/misc.js content/main.js Copyright: 2021 Wojtek Kosior <koszko@koszko.org> 2021 jahoti <jahoti@tilde.team> License: GPL-3+-javascript or Alicense-1.0 diff --git a/html/options_main.js b/html/options_main.js index f7adf39..2eeee1b 100644 --- a/html/options_main.js +++ b/html/options_main.js @@ -11,7 +11,6 @@ * IMPORT TYPE_PREFIX * IMPORT TYPE_NAME * IMPORT list_prefixes - * IMPORT url_extract_target * IMPORT nice_name * IMPORTS_END */ @@ -691,20 +690,20 @@ function initialize_import_facility() */ function jump_to_item(url_with_item) { - const parsed_url = url_extract_target(url_with_item); - - if (parsed_url.target === undefined) + const [dummy1, base_url, dummy2, target] = + /^([^#]*)(#(.*))?$/i.exec(url_with_item); + if (target === undefined) return; - const prefix = parsed_url.target.substring(1, 2); + const prefix = target.substring(0, 1); if (!list_prefixes.includes(prefix)) { - history.replaceState(null, "", parsed_url.base_url); + history.replaceState(null, "", base_url); return; } by_id(`show_${TYPE_NAME[prefix]}s`).checked = true; - edit_item(prefix, decodeURIComponent(parsed_url.target.substring(2))); + edit_item(prefix, decodeURIComponent(target.substring(1))); } async function main() |