diff options
author | Wojtek Kosior <koszko@koszko.org> | 2021-09-09 17:47:51 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2021-09-09 18:50:58 +0200 |
commit | 44e89d8ec71b441a431c848567f34b9a36f6b982 (patch) | |
tree | 62881ff7fc0084bdb8a7c29c10e270a9a3b1245d | |
parent | e2d26bad35bbe3876862b482f7963d713238313b (diff) | |
download | browser-extension-44e89d8ec71b441a431c848567f34b9a36f6b982.tar.gz browser-extension-44e89d8ec71b441a431c848567f34b9a36f6b982.zip |
simplify CSP handling
All page's CSP rules are now removed when a payload is to be injected. When there is no payload, CSP rules are not modified but only supplemented with Hachette's own.
-rw-r--r-- | background/policy_injector.js | 30 | ||||
-rw-r--r-- | background/stream_filter.js | 5 | ||||
-rw-r--r-- | common/misc.js | 68 | ||||
-rw-r--r-- | content/main.js | 57 |
4 files changed, 60 insertions, 100 deletions
diff --git a/background/policy_injector.js b/background/policy_injector.js index 72318d4..e5af055 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -10,9 +10,8 @@ * IMPORTS_START * IMPORT sign_data * IMPORT extract_signed - * IMPORT sanitize_csp_header - * IMPORT csp_rule - * IMPORT is_csp_header_name + * IMPORT make_csp_rule + * IMPORT csp_header_regex * IMPORTS_END */ @@ -43,22 +42,25 @@ function inject_csp_headers(headers, policy) break; } + if (policy.has_payload) { + csp_headers = []; + const non_csp_headers = []; + const header_list = + h => csp_header_regex.test(h) ? csp_headers : non_csp_headers; + headers.forEach(h => header_list(h.name).push(h)); + headers = non_csp_headers; + } else { + headers.push(...csp_headers || []); + } + if (!hachette_header) { hachette_header = {name: "x-hachette"}; headers.push(hachette_header); } - csp_headers = csp_headers || - headers.filter(h => is_csp_header_name(h.name)); - - /* When blocking remove report-only CSP headers that snitch on us. */ - headers = headers.filter(h => !is_csp_header_name(h.name, !policy.allow)); - if (old_signature) headers = headers.filter(h => h.value.search(old_signature) === -1); - headers.push(...csp_headers.map(h => sanitize_csp_header(h, policy))); - const policy_str = encodeURIComponent(JSON.stringify(policy)); const signed_policy = sign_data(policy_str, new Date().getTime()); const later_30sec = new Date(new Date().getTime() + 30000).toGMTString(); @@ -76,12 +78,12 @@ function inject_csp_headers(headers, policy) hachette_data = encodeURIComponent(JSON.stringify(hachette_data)); hachette_header.value = sign_data(hachette_data, 0).join("_"); - /* To ensure there is a CSP header if required */ - if (!policy.allow) + if (!policy.allow) { headers.push({ name: "content-security-policy", - value: csp_rule(policy.nonce) + value: make_csp_rule(policy) }); + } return headers; } diff --git a/background/stream_filter.js b/background/stream_filter.js index 96b6132..3e30a4b 100644 --- a/background/stream_filter.js +++ b/background/stream_filter.js @@ -12,7 +12,7 @@ /* * IMPORTS_START * IMPORT browser - * IMPORT is_csp_header_name + * IMPORT csp_header_regex * IMPORTS_END */ @@ -116,8 +116,7 @@ function may_define_csp_rules(html) const doc = new DOMParser().parseFromString(html, "text/html"); for (const meta of doc.querySelectorAll("head>meta[http-equiv]")) { - if (is_csp_header_name(meta.getAttribute("http-equiv"), true) && - meta.content) + if (csp_header_regex.test(meta.httpEquiv) && meta.content) return true; } diff --git a/common/misc.js b/common/misc.js index 6adaf1e..6cded84 100644 --- a/common/misc.js +++ b/common/misc.js @@ -43,29 +43,19 @@ function gen_nonce(length=16) return Uint8toHex(randomData); } -/* csp rule that blocks all scripts except for those injected by us */ -function csp_rule(nonce) +/* CSP rule that blocks scripts according to policy's needs. */ +function make_csp_rule(policy) { - const rule = `'nonce-${nonce}'`; - return `script-src ${rule}; script-src-elem ${rule}; script-src-attr 'none'; prefetch-src 'none';`; + let rule = "prefetch-src 'none'; script-src-attr 'none';"; + const script_src = policy.has_payload ? + `'nonce-${policy.nonce}'` : "'none'"; + rule += ` script-src ${script_src}; script-src-elem ${script_src};`; + return rule; } /* Check if some HTTP header might define CSP rules. */ -const csp_header_names = new Set([ - "content-security-policy", - "x-webkit-csp", - "x-content-security-policy" -]); - -const report_only_header_name = "content-security-policy-report-only"; - -function is_csp_header_name(string, include_report_only) -{ - string = string && string.toLowerCase().trim() || ""; - - return (include_report_only && string === report_only_header_name) || - csp_header_names.has(string); -} +const csp_header_regex = + /^\s*(content-security-policy|x-webkit-csp|x-content-security-policy)/i; /* * Print item together with type, e.g. @@ -111,41 +101,6 @@ function parse_csp(csp) { return directives; } -/* Make CSP headers do our bidding, not interfere */ -function sanitize_csp_header(header, policy) -{ - const rule = `'nonce-${policy.nonce}'`; - const csp = parse_csp(header.value); - - if (!policy.allow) { - /* No snitching */ - delete csp['report-to']; - delete csp['report-uri']; - - delete csp['script-src']; - delete csp['script-src-elem']; - - csp['script-src-attr'] = ["'none'"]; - csp['prefetch-src'] = ["'none'"]; - } - - if ('script-src' in csp) - csp['script-src'].push(rule); - else - csp['script-src'] = [rule]; - - if ('script-src-elem' in csp) - csp['script-src-elem'].push(rule); - else - csp['script-src-elem'] = [rule]; - - const new_csp = Object.entries(csp).map( - i => `${i[0]} ${i[1].join(' ')};` - ); - - return {name: header.name, value: new_csp.join('')}; -} - /* Regexes and objects to use as/in schemas for parse_json_with_schema(). */ const nonempty_string_matcher = /.+/; @@ -161,12 +116,11 @@ const matchers = { /* * EXPORTS_START * EXPORT gen_nonce - * EXPORT csp_rule - * EXPORT is_csp_header_name + * EXPORT make_csp_rule + * EXPORT csp_header_regex * EXPORT nice_name * EXPORT open_in_settings * EXPORT is_privileged_url - * EXPORT sanitize_csp_header * EXPORT matchers * EXPORTS_END */ diff --git a/content/main.js b/content/main.js index fb334dd..a26f72d 100644 --- a/content/main.js +++ b/content/main.js @@ -16,9 +16,8 @@ * IMPORT is_chrome * IMPORT is_mozilla * IMPORT start_activity_info_server - * IMPORT csp_rule - * IMPORT is_csp_header_name - * IMPORT sanitize_csp_header + * IMPORT make_csp_rule + * IMPORT csp_header_regex * IMPORTS_END */ @@ -172,22 +171,20 @@ function block_attribute(node, attr, ns=null) const name = construct_name.join(""); seta(node, `${blocked_str}-${name}`, geta(node, name)); } -} - -function sanitize_meta(meta, policy) -{ - const value = meta.content || ""; - if (!value || !is_csp_header_name(meta.httpEquiv || "", true)) - return; - - block_attribute(meta, "content"); + rema(node, attr); } /* - * Used to disable <script> that has not yet been added to live DOM (doesn't - * work for those already added). + * Used to disable `<script>'s and `<meta>'s that have not yet been added to + * live DOM (doesn't work for those already added). */ +function sanitize_meta(meta) +{ + if (csp_header_regex.test(meta.httpEquiv) && meta.content) + block_attribute(meta, "content"); +} + function sanitize_script(script) { script.hachette_blocked_type = script.getAttribute("type"); @@ -195,14 +192,14 @@ function sanitize_script(script) } /* - * Executed after script has been connected to the DOM, when it is no longer - * eligible for being executed by the browser + * Executed after `<script>' has been connected to the DOM, when it is no longer + * eligible for being executed by the browser. */ -function desanitize_script(script, policy) +function desanitize_script(script) { script.setAttribute("type", script.hachette_blocked_type); - if (script.hachette_blocked_type === null) + if ([null, undefined].includes(script.hachette_blocked_type)) script.removeAttribute("type"); delete script.hachette_blocked_type; @@ -233,13 +230,18 @@ function start_data_urls_sanitizing(doc) * cause part of the DOM to be loaded when our content scripts get to run. Thus, * before the CSP rules we inject (for non-HTTP pages) become effective, we need * to somehow block the execution of `<script>'s and intrinsics that were - * already there. + * already there. Additionally, some browsers (IceCat 60) seem to have problems + * applying this CSP to non-inline `<scripts>' in certain scenarios. */ +function prevent_script_execution(event) +{ + if (!event.target._hachette_payload) + event.preventDefault(); +} + function mozilla_initial_block(doc) { - const blocker = e => e.preventDefault(); - doc.addEventListener("beforescriptexecute", blocker); - setTimeout(() => doc.removeEventListener("beforescriptexecute", blocker)); + doc.addEventListener("beforescriptexecute", prevent_script_execution); [...doc.all].flatMap(ele => [...ele.attributes].map(attr => [ele, attr])) .map(([ele, attr]) => [ele, attr.localName]) @@ -273,7 +275,7 @@ async function sanitize_document(doc, policy) * non-HTML documents. */ const html = new DOMParser().parseFromString(`<html><head><meta \ -http-equiv="Content-Security-Policy" content="${csp_rule(policy.nonce)}"\ +http-equiv="Content-Security-Policy" content="${make_csp_rule(policy)}"\ /></head><body>Loading...</body></html>`, "text/html").documentElement; /* @@ -284,10 +286,10 @@ http-equiv="Content-Security-Policy" content="${csp_rule(policy.nonce)}"\ root.replaceWith(html); /* - * For XML documents, we don't intend to inject payload, so we neither block - * document's CSP `<meta>' tags nor wait for `<head>' to be parsed. + * When we don't inject payload, we neither block document's CSP `<meta>' + * tags nor wait for `<head>' to be parsed. */ - if (document instanceof HTMLDocument) { + if (policy.has_payload) { await wait_for_head(doc, root); root.querySelectorAll("head meta") @@ -333,6 +335,9 @@ if (!is_privileged_url(document.URL)) { policy = {allow: false, nonce: gen_nonce()}; } + if (!(document instanceof HTMLDocument)) + policy.has_payload = false; + console.debug("current policy", policy); const doc_ready = Promise.all([ |