summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--background/nonce_store.js30
-rw-r--r--background/page_actions_server.js2
-rw-r--r--background/policy_injector.js93
-rw-r--r--common/misc.js64
-rw-r--r--content/freezer.js2
-rw-r--r--content/main.js73
-rw-r--r--content/page_actions.js5
-rw-r--r--copyright6
-rw-r--r--html/options_main.js13
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});
diff --git a/copyright b/copyright
index 6282043..b0c63d5 100644
--- a/copyright
+++ b/copyright
@@ -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()