aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-09-09 17:47:51 +0200
committerWojtek Kosior <koszko@koszko.org>2021-09-09 18:50:58 +0200
commit44e89d8ec71b441a431c848567f34b9a36f6b982 (patch)
tree62881ff7fc0084bdb8a7c29c10e270a9a3b1245d
parente2d26bad35bbe3876862b482f7963d713238313b (diff)
downloadbrowser-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.js30
-rw-r--r--background/stream_filter.js5
-rw-r--r--common/misc.js68
-rw-r--r--content/main.js57
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([