diff options
author | Wojtek Kosior <koszko@koszko.org> | 2021-07-20 10:17:19 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2021-07-20 10:17:19 +0200 |
commit | 0c7c1ebddab49e1e0b1ad4cc4c8fcdeedd220946 (patch) | |
tree | 1afd10275310177cf28991ad021cfb74e4add9f3 | |
parent | 1789f17466847d731d0bafa67b6d76526ca32b1d (diff) | |
parent | ecb787046271de708b94da70240713e725299d86 (diff) | |
download | browser-extension-0c7c1ebddab49e1e0b1ad4cc4c8fcdeedd220946.tar.gz browser-extension-0c7c1ebddab49e1e0b1ad4cc4c8fcdeedd220946.zip |
Merge commit 'ecb787046271de708b94da70240713e725299d86'
-rw-r--r-- | background/nonce_store.js | 30 | ||||
-rw-r--r-- | background/page_actions_server.js | 2 | ||||
-rw-r--r-- | background/policy_injector.js | 93 | ||||
-rw-r--r-- | common/misc.js | 64 | ||||
-rw-r--r-- | content/freezer.js | 2 | ||||
-rw-r--r-- | content/main.js | 73 | ||||
-rw-r--r-- | content/page_actions.js | 5 | ||||
-rw-r--r-- | copyright | 6 | ||||
-rw-r--r-- | html/options_main.js | 13 |
9 files changed, 154 insertions, 134 deletions
diff --git a/background/nonce_store.js b/background/nonce_store.js deleted file mode 100644 index e5a0e78..0000000 --- a/background/nonce_store.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Central management of HTTP(S) nonces - * - * Copyright (C) 2021 jahoti - * Redistribution terms are gathered in the `copyright' file. - */ - -/* - * IMPORTS_START - * IMPORT gen_nonce - * IMPORTS_END - */ - -var nonces = {}; - -function retrieve_nonce(tabId, frameId, update) -{ - let code = tabId + '.' + frameId; - console.log('Nonce for ' + code + ' ' + (update ? 'created/updated' : 'requested')); - if (update) - nonces[code] = gen_nonce(); - - return nonces[code]; -} - -/* - * EXPORTS_START - * EXPORT retrieve_nonce - * EXPORTS_END - */ diff --git a/background/page_actions_server.js b/background/page_actions_server.js index d92b870..2d9c333 100644 --- a/background/page_actions_server.js +++ b/background/page_actions_server.js @@ -11,7 +11,6 @@ * IMPORT TYPE_PREFIX * IMPORT CONNECTION_TYPE * IMPORT browser - * IMPORT retrieve_nonce * IMPORT listen_for_connection * IMPORT sha256 * IMPORT get_query_best @@ -138,7 +137,6 @@ function handle_message(port, message, handler) function new_connection(port) { console.log("new page actions connection!"); - port.postMessage(['nonce', retrieve_nonce((port.sender.tab || '').id, port.sender.frameId)]); let handler = []; handler.push(m => handle_message(port, m, handler)); port.onMessage.addListener(handler[0]); diff --git a/background/policy_injector.js b/background/policy_injector.js index 9f79425..ee97333 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -2,6 +2,7 @@ * Myext injecting policy to page using webRequest * * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ @@ -11,8 +12,13 @@ * IMPORT get_storage * IMPORT browser * IMPORT is_chrome - * IMPORT retrieve_nonce + * IMPORT is_mozilla + * IMPORT gen_unique + * IMPORT gen_nonce + * IMPORT is_privileged_url * IMPORT url_item + * IMPORT url_extract_target + * IMPORT sign_policy * IMPORT get_query_best * IMPORT csp_rule * IMPORTS_END @@ -34,34 +40,60 @@ function is_csp_header(header) return !!csp_header_names[header.name.toLowerCase()]; } -function is_our_header(header, rule) +function url_inject(details) { - return header.value === rule + if (is_privileged_url(details.url)) + return; + + 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) + settings = {}; + + const policy = encodeURIComponent( + JSON.stringify({ + allow: settings.allow, + nonce: gen_nonce(), + base_url: targets.base_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 url = url_item(details.url); - - const [pattern, settings] = query_best(url); - - const nonce = retrieve_nonce(details.tabId, details.frameId, true); - const rule = csp_rule(nonce); - - var headers; - - if (settings !== undefined && settings.allow) { - /* - * Chrome doesn't have the buggy behavior of repeatedly injecting a - * header we injected once. Firefox does and we have to remove it there. - */ - if (is_chrome) - return {cancel: false}; - - headers = details.responseHeaders.filter(h => !is_our_header(h, rule)); - } else { - headers = details.responseHeaders.filter(h => !is_csp_header(h)); - + const targets = url_extract_target(details.url); + /* Block mis-/unsigned requests */ + if (!targets.current) + 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) + headers = headers.filter(h => !is_csp_header(h)); + + if (!targets.policy.allow) { headers.push({ name : header_name, value : rule @@ -80,8 +112,17 @@ async function start_policy_injector() if (is_chrome) extra_opts.push("extraHeaders"); + browser.webRequest.onBeforeRequest.addListener( + url_inject, + { + urls: ["<all_urls>"], + types: ["main_frame", "sub_frame"] + }, + ["blocking"] + ); + browser.webRequest.onHeadersReceived.addListener( - inject, + headers_inject, { urls: ["<all_urls>"], types: ["main_frame", "sub_frame"] diff --git a/common/misc.js b/common/misc.js index 7e48059..8cb26ab 100644 --- a/common/misc.js +++ b/common/misc.js @@ -15,6 +15,14 @@ * IMPORTS_END */ +/* Generate a random base64-encoded 128-bit sequence */ +function gen_nonce() +{ + let randomData = new Uint8Array(16); + crypto.getRandomValues(randomData); + return btoa(String.fromCharCode.apply(null, randomData)); +} + /* * generating unique, per-site value that can be computed synchronously * and is impossible to guess for a malicious website @@ -61,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 */ @@ -113,12 +148,19 @@ function is_privileged_url(url) return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url); } +/* Sign a given policy for a given time */ +function sign_policy(policy, now, hours_offset) { + let time = Math.floor(now / 3600000) + (hours_offset || 0); + return gen_unique(time + policy); +} + /* * EXPORTS_START - * EXPORT gen_unique * EXPORT gen_nonce + * EXPORT gen_unique * EXPORT url_item * EXPORT url_extract_target + * EXPORT sign_policy * EXPORT csp_rule * EXPORT nice_name * EXPORT open_in_settings 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 b044c82..af8cd7c 100644 --- a/content/main.js +++ b/content/main.js @@ -8,7 +8,6 @@ /* * IMPORTS_START - * IMPORT CONNECTION_TYPE * IMPORT handle_page_actions * IMPORT url_item * IMPORT url_extract_target @@ -18,7 +17,6 @@ * IMPORT is_privileged_url * IMPORT sanitize_attributes * IMPORT mozilla_suppress_scripts - * IMPORT browser * IMPORT is_chrome * IMPORT is_mozilla * IMPORT start_activity_info_server @@ -28,39 +26,15 @@ /* * 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. */ -let url = url_item(document.URL); -let unique = gen_unique(url); - - -function is_http() -{ - return !!/^https?:\/\//i.exec(document.URL); -} - -function is_whitelisted() -{ - const parsed_url = url_extract_target(document.URL); - - if (parsed_url.target !== undefined && - parsed_url.target === '#' + unique) { - if (parsed_url.target2 !== undefined) - window.location.href = parsed_url.base_url + parsed_url.target2; - else - history.replaceState(null, "", parsed_url.base_url); - - return true; - } - - return false; -} +var nonce = undefined; function handle_mutation(mutations, observer) { @@ -85,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); @@ -126,24 +99,20 @@ function inject_csp(head) } if (!is_privileged_url(document.URL)) { - start_activity_info_server(); - var nonce, port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); - - if (is_http()) { - /* rely on CSP injected through webRequest, at the cost of having to fetch a nonce via messaging */ - const nonce_capturer = msg => { - port.onMessage.removeListener(nonce_capturer); - handle_page_actions(msg[1], port); - }; - - port.onMessage.addListener(nonce_capturer); - - } else if (is_whitelisted()) { - /* do not block scripts at all; as a result, there is no need for a green-lighted nonce */ - handle_page_actions(null, port); - } else { - nonce = gen_nonce(); - handle_page_actions(nonce, port); + const targets = url_extract_target(document.URL); + if (targets.policy) { + if (targets.target2) + window.location.href = targets.base_url + targets.target2; + else + history.replaceState(null, "", targets.base_url); + } + + const policy = targets.current ? targets.policy : {}; + + nonce = policy.nonce || gen_nonce(); + handle_page_actions(nonce); + + if (!policy.allow) { block_nodes_recursively(document.documentElement); if (is_chrome) { @@ -158,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 dff5f71..75cc4d9 100644 --- a/content/page_actions.js +++ b/content/page_actions.js @@ -7,6 +7,7 @@ /* * IMPORTS_START + * IMPORT CONNECTION_TYPE * IMPORT browser * IMPORT report_script * IMPORT report_settings @@ -49,13 +50,15 @@ 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); } -function handle_page_actions(script_nonce, port) { // Add port as an argument so we can "pre-receive" a nonce in main.js +function handle_page_actions(script_nonce) { document.addEventListener("DOMContentLoaded", document_loaded); + port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); port.onMessage.addListener(handle_message); port.postMessage({url: document.URL}); @@ -11,7 +11,7 @@ Copyright: 2021 Wojtek Kosior <koszko@koszko.org> 2021 jahoti <jahoti@tilde.team> License: CC0 -Files: 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 @@ -47,10 +47,6 @@ License: Expat OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: background/nonce_store.js -Copyright: 2021 jahoti <jahoti@tilde.team> -License: GPL-3+-javascript or Alicense-1.0 - Files: content/freezer.js Copyright: 2005-2021 Giorgio Maone - https://maone.net 2021 jahoti <jahoti@tilde.team> 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() |