summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjahoti <jahoti@tilde.team>2021-07-25 00:00:00 +0000
committerWojtek Kosior <koszko@koszko.org>2021-07-26 11:09:57 +0200
commit97b8e30fadf0f1e1e0aeb9078ac333026d735270 (patch)
treecc1b4b85c50eca17eb27654a82488352923b48a3
parente402e0363cd55f7f849c20c1acd96de548ebc9a6 (diff)
downloadbrowser-extension-97b8e30fadf0f1e1e0aeb9078ac333026d735270.tar.gz
browser-extension-97b8e30fadf0f1e1e0aeb9078ac333026d735270.zip
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.
-rw-r--r--background/policy_injector.js133
1 files changed, 98 insertions, 35 deletions
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};