From 014f2a2f4e2071c35314d67285711f0f4615266b Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 18 Aug 2021 17:53:57 +0200 Subject: implement smuggling via cookies instead of URL --- background/policy_injector.js | 190 +++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 125 deletions(-) (limited to 'background') 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: [""], - types: ["main_frame", "sub_frame"] - }, - ["blocking"] - ); - browser.webRequest.onHeadersReceived.addListener( headers_inject, { -- cgit v1.2.3