diff options
author | Wojtek Kosior <koszko@koszko.org> | 2021-08-18 17:53:57 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2021-08-18 17:53:57 +0200 |
commit | 014f2a2f4e2071c35314d67285711f0f4615266b (patch) | |
tree | 081c18c6fc1270d1e312962bd21b71a7072004c4 | |
parent | 0bbda8fceb52f28032460db0331b09ad086a2a64 (diff) | |
download | browser-extension-014f2a2f4e2071c35314d67285711f0f4615266b.tar.gz browser-extension-014f2a2f4e2071c35314d67285711f0f4615266b.zip |
implement smuggling via cookies instead of URL
-rw-r--r-- | background/policy_injector.js | 190 | ||||
-rw-r--r-- | common/misc.js | 84 | ||||
-rw-r--r-- | content/main.js | 24 | ||||
-rw-r--r-- | html/display-panel.js | 3 |
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; |