From 97b8e30fadf0f1e1e0aeb9078ac333026d735270 Mon Sep 17 00:00:00 2001 From: jahoti Date: Sun, 25 Jul 2021 00:00:00 +0000 Subject: Squash more CSP-filtering bugs On Firefox, original CSP headers are now smuggled (signed) in an x-orig-csp header to prevent re-processing issues with caching. Additionally, a default header is added for non-whitelisted domains in case there are no existing headers we can attach to. --- background/policy_injector.js | 133 +++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 35 deletions(-) (limited to 'background') diff --git a/background/policy_injector.js b/background/policy_injector.js index 90c65bd..f58fb71 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -12,6 +12,7 @@ * IMPORT get_storage * IMPORT browser * IMPORT is_chrome + * IMPORT is_mozilla * IMPORT gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url @@ -39,7 +40,7 @@ const unwanted_csp_directives = { "prefetch-src": true }; -const header_name = "content-security-policy"; +const report_only = "content-security-policy-report-only"; function not_csp_header(header) { @@ -82,6 +83,38 @@ function url_inject(details) }; } +function process_csp_header(header, rule, block) +{ + const csp = parse_csp(header.value); + + /* No snitching */ + delete csp['report-to']; + delete csp['report-uri']; + + if (block) { + 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_policy = Object.entries(csp).map( + i => i[0] + ' ' + i[1].join(' ') + ';' + ); + + return {name: header.name, value: new_policy.join('')} +} + function headers_inject(details) { const targets = url_extract_target(details.url); @@ -89,48 +122,78 @@ function headers_inject(details) if (!targets.current) return {cancel: true}; - const headers = []; + 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 (let header of details.responseHeaders) { if (not_csp_header(header)) { /* Retain all non-snitching headers */ - if (header.name.toLowerCase() !== - 'content-security-policy-report-only') + if (header.name.toLowerCase() !== report_only) { 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 (let header of data) + headers.push(process_csp_header(header, rule, block)); + } + } continue; } - - const csp = parse_csp(header.value); - const rule = `'nonce-${targets.policy.nonce}'` - - /* TODO: confirm deleting non-existent things is OK everywhere */ - /* No snitching or prefetching/rendering */ - delete csp['report-to']; - delete csp['report-uri']; - - if (!targets.policy.allow) { - 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]; - - /* TODO: is this safe */ - let new_policy = Object.entries(csp).map( - i => i[0] + ' ' + i[1].join(' ') + ';' - ); + if (is_mozilla && !orig_csp_headers) + continue; - headers.push({name: header.name, value: new_policy.join('')}); + csp_headers.push(process_csp_header(header, rule, block)); + if (is_mozilla) + orig_csp_headers.push(header); + } + + 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); + } + + /* 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';" + }); } return {responseHeaders: headers}; -- cgit v1.2.3