summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-08-18 17:53:57 +0200
committerWojtek Kosior <koszko@koszko.org>2021-08-18 17:53:57 +0200
commit014f2a2f4e2071c35314d67285711f0f4615266b (patch)
tree081c18c6fc1270d1e312962bd21b71a7072004c4
parent0bbda8fceb52f28032460db0331b09ad086a2a64 (diff)
downloadbrowser-extension-014f2a2f4e2071c35314d67285711f0f4615266b.tar.gz
browser-extension-014f2a2f4e2071c35314d67285711f0f4615266b.zip
implement smuggling via cookies instead of URL
-rw-r--r--background/policy_injector.js190
-rw-r--r--common/misc.js84
-rw-r--r--content/main.js24
-rw-r--r--html/display-panel.js3
4 files changed, 96 insertions, 205 deletions
diff --git a/background/policy_injector.js b/background/policy_injector.js
index 9725e99..947812e 100644
--- a/background/policy_injector.js
+++ b/background/policy_injector.js
@@ -8,19 +8,16 @@
/*
* IMPORTS_START
- * IMPORT TYPE_PREFIX
* IMPORT get_storage
* IMPORT browser
* IMPORT is_chrome
- * IMPORT is_mozilla
- * IMPORT gen_unique
* IMPORT gen_nonce
* IMPORT is_privileged_url
- * IMPORT url_item
- * IMPORT url_extract_target
- * IMPORT sign_policy
+ * IMPORT sign_data
+ * IMPORT extract_signed
* IMPORT query_best
* IMPORT sanitize_csp_header
+ * IMPORT csp_rule
* IMPORTS_END
*/
@@ -32,129 +29,81 @@ const csp_header_names = new Set([
"x-content-security-policy"
]);
-/* TODO: variable no longer in use; remove if not needed */
-const unwanted_csp_directives = new Set([
- "report-to",
- "report-uri",
- "script-src",
- "script-src-elem",
- "prefetch-src"
-]);
-
const report_only = "content-security-policy-report-only";
-function url_inject(details)
+function headers_inject(details)
{
- if (is_privileged_url(details.url))
+ console.log("ijnector details", details);
+ const url = details.url;
+ if (is_privileged_url(url))
return;
- const targets = url_extract_target(details.url);
- if (targets.current)
- return;
+ const [pattern, settings] = query_best(storage, url);
+ const allow = !!(settings && settings.allow);
+ const nonce = gen_nonce();
+ const rule = `'nonce-${nonce}'`;
- /* Redirect; update policy */
- if (targets.policy)
- targets.target = "";
-
- let [pattern, settings] = query_best(storage, targets.base_url);
- /* Defaults */
- if (!pattern)
- settings = {};
-
- const policy = encodeURIComponent(
- JSON.stringify({
- allow: settings.allow,
- nonce: gen_nonce(),
- base_url: targets.base_url
- })
- );
+ let orig_csp_headers;
+ let old_signature;
+ let hachette_header;
+ let headers = details.responseHeaders;
- return {
- redirectUrl: [
- targets.base_url,
- '#', sign_policy(policy, new Date()), policy,
- targets.target,
- targets.target2
- ].join("")
- };
-}
+ for (const header of headers.filter(h => h.name === "x-hachette")) {
+ const match = /^([^%])(%.*)$/.exec(header.value);
+ if (!match)
+ continue;
-function headers_inject(details)
-{
- const targets = url_extract_target(details.url);
- /* Block mis-/unsigned requests */
- if (!targets.current)
- return {cancel: true};
-
- let orig_csp_headers = is_chrome ? null : [];
- let headers = [];
- let csp_headers = is_chrome ? headers : [];
-
- const rule = `'nonce-${targets.policy.nonce}'`;
- const block = !targets.policy.allow;
-
- for (const header of details.responseHeaders) {
- if (!csp_header_names.has(header)) {
- /* Remove headers that only snitch on us */
- if (header.name.toLowerCase() === report_only && block)
- continue;
- headers.push(header);
-
- /* If these are the original CSP headers, use them instead */
- /* Test based on url_extract_target() in misc.js */
- if (is_mozilla && header.name === "x-orig-csp") {
- let index = header.value.indexOf('%5B');
- if (index === -1)
- continue;
-
- let sig = header.value.substring(0, index);
- let data = header.value.substring(index);
- if (sig !== sign_policy(data, 0))
- continue;
-
- /* Confirmed- it's the originals, smuggled in! */
- try {
- data = JSON.parse(decodeURIComponent(data));
- } catch (e) {
- /* This should not be reached -
- it's our self-produced valid JSON. */
- console.log("Unexpected internal error - invalid JSON smuggled!", e);
- }
-
- orig_csp_headers = csp_headers = null;
- for (const header of data)
- headers.push(sanitize_csp_header(header, rule, block));
- }
- } else if (is_chrome || !orig_csp_headers) {
- csp_headers.push(sanitize_csp_header(header, rule, block));
- if (is_mozilla)
- orig_csp_headers.push(header);
- }
+ const old_data = extract_signed(...match.splice(1, 2), [[0]]);
+ if (!old_data || old_data.url !== url)
+ continue;
+
+ /* Confirmed- it's the originals, smuggled in! */
+ orig_csp_headers = old_data.csp_headers;
+ old_signature = old_data.policy_signature;
+
+ hachette_header = header;
+ break;
}
- if (orig_csp_headers) {
- /** Smuggle in the original CSP headers for future use.
- * These are signed with a time of 0, as it's not clear there
- * is a limit on how long Firefox might retain these headers in
- * the cache.
- */
- orig_csp_headers = encodeURIComponent(JSON.stringify(orig_csp_headers));
- headers.push({
- name: "x-orig-csp",
- value: sign_policy(orig_csp_headers, 0) + orig_csp_headers
- });
-
- headers = headers.concat(csp_headers);
+ if (!hachette_header) {
+ hachette_header = {name: "x-hachette"};
+ headers.push(hachette_header);
}
+ orig_csp_headers ||=
+ headers.filter(h => csp_header_names.has(h.name.toLowerCase()));
+ headers = headers.filter(h => !csp_header_names.has(h.name.toLowerCase()));
+
+ /* Remove headers that only snitch on us */
+ if (!allow)
+ headers = headers.filter(h => h.name.toLowerCase() !== report_only);
+
+ if (old_signature)
+ headers = headers.filter(h => h.name.search(old_signature) === -1);
+
+ const sanitizer = h => sanitize_csp_header(h, rule, allow);
+ headers.push(...orig_csp_headers.map(sanitizer));
+
+ const policy = encodeURIComponent(JSON.stringify({allow, nonce, url}));
+ const policy_signature = sign_data(policy, new Date());
+ const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();
+ headers.push({
+ name: "Set-Cookie",
+ value: `hachette-${policy_signature}=${policy}; Expires=${later_30sec};`
+ });
+
+ /*
+ * Smuggle in the signature and the original CSP headers for future use.
+ * These are signed with a time of 0, as it's not clear there is a limit on
+ * how long Firefox might retain headers in the cache.
+ */
+ let hachette_data = {csp_headers: orig_csp_headers, policy_signature, url};
+ hachette_data = encodeURIComponent(JSON.stringify(hachette_data));
+ hachette_header.value = sign_data(hachette_data, 0) + hachette_data;
+
/* To ensure there is a CSP header if required */
- if (block) {
- headers.push({
- name: "content-security-policy",
- value: `script-src ${rule}; script-src-elem ${rule}; ` +
- "script-src-attr 'none'; prefetch-src 'none';"
- });
- }
+ if (!allow)
+ headers.push({name: "content-security-policy", value: csp_rule(nonce)});
return {responseHeaders: headers};
}
@@ -167,15 +116,6 @@ 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(
headers_inject,
{
diff --git a/common/misc.js b/common/misc.js
index 3c7dc46..39c696f 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -45,11 +45,6 @@ function gen_nonce(length) // Default 16
return Uint8toHex(randomData);
}
-function gen_unique(url)
-{
- return sha256(get_secure_salt() + url);
-}
-
function get_secure_salt()
{
if (is_chrome)
@@ -58,72 +53,29 @@ function get_secure_salt()
return browser.runtime.getURL("dummy");
}
-/*
- * stripping url from query and target (everything after `#' or `?'
- * gets removed)
- */
-function url_item(url)
-{
- let url_re = /^([^?#]*).*$/;
- let match = url_re.exec(url);
- return match[1];
-}
-
-/*
- * 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": "#<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)
+function extract_signed(signature, data, times)
{
- 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;
+ times ||= [[now], [now, -1]];
+
+ const reductor =
+ (ok, time) => ok || signature === sign_data(data, ...time);
+ if (!times.reduce(reductor, false))
+ return undefined;
try {
- targets.policy = JSON.parse(decodeURIComponent(policy));
- targets.current = targets.policy.base_url === targets.base_url;
+ return JSON.parse(decodeURIComponent(data));
} 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 */
function csp_rule(nonce)
{
- let rule = `script-src 'nonce-${nonce}';`;
- if (is_chrome)
- rule += `script-src-elem 'nonce-${nonce}';`;
- return rule;
+ const rule = `'nonce-${nonce}'`;
+ return `script-src ${rule}; script-src-elem ${rule}; script-src-attr 'none'; prefetch-src 'none';`;
}
/*
@@ -149,10 +101,10 @@ 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) {
+/* Sign a given string for a given time */
+function sign_data(data, now, hours_offset) {
let time = Math.floor(now / 3600000) + (hours_offset || 0);
- return gen_unique(time + policy);
+ return sha256(get_secure_salt() + time + data);
}
/* Parse a CSP header */
@@ -175,11 +127,11 @@ function parse_csp(csp) {
}
/* Make CSP headers do our bidding, not interfere */
-function sanitize_csp_header(header, rule, block)
+function sanitize_csp_header(header, rule, allow)
{
const csp = parse_csp(header.value);
- if (block) {
+ if (!allow) {
/* No snitching */
delete csp['report-to'];
delete csp['report-uri'];
@@ -223,10 +175,8 @@ const matchers = {
/*
* EXPORTS_START
* EXPORT gen_nonce
- * EXPORT gen_unique
- * EXPORT url_item
- * EXPORT url_extract_target
- * EXPORT sign_policy
+ * EXPORT extract_signed
+ * EXPORT sign_data
* EXPORT csp_rule
* EXPORT nice_name
* EXPORT open_in_settings
diff --git a/content/main.js b/content/main.js
index 9ed557c..8adcd48 100644
--- a/content/main.js
+++ b/content/main.js
@@ -9,8 +9,7 @@
/*
* IMPORTS_START
* IMPORT handle_page_actions
- * IMPORT url_extract_target
- * IMPORT gen_unique
+ * IMPORT extract_signed
* IMPORT gen_nonce
* IMPORT csp_rule
* IMPORT is_privileged_url
@@ -98,18 +97,21 @@ function inject_csp(head)
}
if (!is_privileged_url(document.URL)) {
- 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 reductor =
+ (ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
+ const matches = [...document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)];
+ let [policy, signature] = matches.reduce(reductor, []);
+
+ console.log("extracted policy", [signature, policy]);
+ if (!policy || policy.url !== document.URL) {
+ console.log("using default policy");
+ policy = {allow: false, nonce: gen_nonce()};
}
- const policy = targets.current ? targets.policy : {};
+ if (signature)
+ document.cookie = `hachette-${signature}=; Max-Age=-1;`;
- nonce = policy.nonce || gen_nonce();
- handle_page_actions(nonce);
+ handle_page_actions(policy.nonce);
if (!policy.allow) {
block_nodes_recursively(document.documentElement);
diff --git a/html/display-panel.js b/html/display-panel.js
index b4d9abb..2539ded 100644
--- a/html/display-panel.js
+++ b/html/display-panel.js
@@ -16,7 +16,6 @@
* IMPORT get_import_frame
* IMPORT query_all
* IMPORT CONNECTION_TYPE
- * IMPORT url_item
* IMPORT is_privileged_url
* IMPORT TYPE_PREFIX
* IMPORT nice_name
@@ -60,7 +59,7 @@ async function show_page_activity_info()
return;
}
- tab_url = url_item(tab.url);
+ tab_url = /^([^?#]*)/.exec(tab.url)[1];
page_url_heading.textContent = tab_url;
if (is_privileged_url(tab_url)) {
show_privileged_notice_chbx.checked = true;