aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--background/policy_injector.js101
-rw-r--r--common/misc.js54
-rw-r--r--content/main.js1
3 files changed, 133 insertions, 23 deletions
diff --git a/background/policy_injector.js b/background/policy_injector.js
index b3d85e8..9725e99 100644
--- a/background/policy_injector.js
+++ b/background/policy_injector.js
@@ -20,24 +20,28 @@
* IMPORT url_extract_target
* IMPORT sign_policy
* IMPORT query_best
- * IMPORT csp_rule
+ * IMPORT sanitize_csp_header
* IMPORTS_END
*/
var storage;
-const csp_header_names = {
- "content-security-policy" : true,
- "x-webkit-csp" : true,
- "x-content-security-policy" : true
-};
+const csp_header_names = new Set([
+ "content-security-policy",
+ "x-webkit-csp",
+ "x-content-security-policy"
+]);
-const header_name = "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"
+]);
-function is_csp_header(header)
-{
- return !!csp_header_names[header.name.toLowerCase()];
-}
+const report_only = "content-security-policy-report-only";
function url_inject(details)
{
@@ -82,20 +86,73 @@ function headers_inject(details)
if (!targets.current)
return {cancel: true};
- const rule = csp_rule(targets.policy.nonce);
- var headers = details.responseHeaders;
+ 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);
+ }
+ }
- /*
- * Chrome doesn't have the buggy behavior of caching headers
- * we injected. Firefox does and we have to remove it there.
- */
- if (!targets.policy.allow || is_mozilla)
- headers = headers.filter(h => !is_csp_header(h));
+ 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 (!targets.policy.allow) {
+ /* To ensure there is a CSP header if required */
+ if (block) {
headers.push({
- name : header_name,
- value : rule
+ name: "content-security-policy",
+ value: `script-src ${rule}; script-src-elem ${rule}; ` +
+ "script-src-attr 'none'; prefetch-src 'none';"
});
}
diff --git a/common/misc.js b/common/misc.js
index 7158d32..3c7dc46 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -155,6 +155,59 @@ function sign_policy(policy, now, hours_offset) {
return gen_unique(time + policy);
}
+/* Parse a CSP header */
+function parse_csp(csp) {
+ let directive, directive_array;
+ let directives = {};
+ for (directive of csp.split(';')) {
+ directive = directive.trim();
+ if (directive === '')
+ continue;
+
+ directive_array = directive.split(/\s+/);
+ directive = directive_array.shift();
+ /* The "true" case should never occur; nevertheless... */
+ directives[directive] = directive in directives ?
+ directives[directive].concat(directive_array) :
+ directive_array;
+ }
+ return directives;
+}
+
+/* Make CSP headers do our bidding, not interfere */
+function sanitize_csp_header(header, rule, block)
+{
+ const csp = parse_csp(header.value);
+
+ if (block) {
+ /* 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_policy = Object.entries(csp).map(
+ i => `${i[0]} ${i[1].join(' ')};`
+ );
+
+ return {name: header.name, value: new_policy.join('')};
+}
+
/* Regexes and objest to use as/in schemas for parse_json_with_schema(). */
const nonempty_string_matcher = /.+/;
@@ -178,6 +231,7 @@ const matchers = {
* 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 8f8375e..9ed557c 100644
--- a/content/main.js
+++ b/content/main.js
@@ -9,7 +9,6 @@
/*
* IMPORTS_START
* IMPORT handle_page_actions
- * IMPORT url_item
* IMPORT url_extract_target
* IMPORT gen_unique
* IMPORT gen_nonce