aboutsummaryrefslogtreecommitdiff
path: root/content/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'content/main.js')
-rw-r--r--content/main.js155
1 files changed, 143 insertions, 12 deletions
diff --git a/content/main.js b/content/main.js
index 4ae7738..8440eb5 100644
--- a/content/main.js
+++ b/content/main.js
@@ -16,7 +16,9 @@
* IMPORT is_chrome
* IMPORT is_mozilla
* IMPORT start_activity_info_server
- * IMPORT modify_on_the_fly
+ * IMPORT csp_rule
+ * IMPORT is_csp_header_name
+ * IMPORT sanitize_csp_header
* IMPORTS_END
*/
@@ -31,6 +33,143 @@ function accept_node(node, parent)
parent.hachette_corresponding.appendChild(clone);
}
+/*
+ * 1. When injecting some payload we need to sanitize <meta> CSP tags before
+ * they reach the document.
+ * 2. Only <meta> tags inside <head> are considered valid by the browser and
+ * need to be considered.
+ * 3. We want to detach <html> from document, wait until its <head> completes
+ * loading, sanitize it and re-attach <html>.
+ * 4. Browsers are eager to add <meta>'s that appear after `</head>' but before
+ * `<body>'. Due to this behavior the `DOMContentLoaded' event is considered
+ * unreliable (although it could still work properly, it is just problematic
+ * to verify).
+ * 5. We shall wait for anything to appear in or after <body> and take that as
+ * a sign <head> has _really_ finished loading.
+ */
+
+function make_body_start_observer(DOM_element, waiting)
+{
+ const observer = new MutationObserver(() => try_body_started(waiting));
+ observer.observe(DOM_element, {childList: true});
+ return observer;
+}
+
+function try_body_started(waiting)
+{
+ const body = waiting.detached_html.querySelector("body");
+
+ if ((body && (body.firstChild || body.nextSibling)) ||
+ waiting.doc.documentElement.nextSibling) {
+ finish_waiting(waiting);
+ return true;
+ }
+
+ if (body && waiting.observers.length < 2)
+ waiting.observers.push(make_body_start_observer(body, waiting));
+}
+
+function finish_waiting(waiting)
+{
+ waiting.observers.forEach(observer => observer.disconnect());
+ waiting.doc.removeEventListener("DOMContentLoaded", waiting.loaded_cb);
+ setTimeout(waiting.callback, 0);
+}
+
+function _wait_for_head(doc, detached_html, callback)
+{
+ const waiting = {doc, detached_html, callback, observers: []};
+ if (try_body_started(waiting))
+ return;
+
+ waiting.observers = [make_body_start_observer(detached_html, waiting)];
+ waiting.loaded_cb = () => finish_waiting(waiting);
+ doc.addEventListener("DOMContentLoaded", waiting.loaded_cb);
+}
+
+function wait_for_head(doc, detached_html)
+{
+ return new Promise(cb => _wait_for_head(doc, detached_html, cb));
+}
+
+const blocked_str = "blocked";
+
+function block_attribute(node, attr)
+{
+ /*
+ * Disabling attributes this way allows them to still be relatively
+ * easily accessed in case they contain some useful data.
+ */
+ const construct_name = [attr];
+ while (node.hasAttribute(construct_name.join("")))
+ construct_name.unshift(blocked_str);
+
+ while (construct_name.length > 1) {
+ construct_name.shift();
+ const name = construct_name.join("");
+ node.setAttribute(`${blocked_str}-${name}`, node.getAttribute(name));
+ }
+
+ node.removeAttribute(attr);
+}
+
+function sanitize_meta(meta, policy)
+{
+ const http_equiv = meta.getAttribute("http-equiv");
+ const value = meta.content;
+
+ if (!value || !is_csp_header_name(http_equiv, true))
+ return;
+
+ block_attribute(meta, "content");
+
+ if (is_csp_header_name(http_equiv, false))
+ meta.content = sanitize_csp_header({value}, policy).value;
+}
+
+function apply_hachette_csp_rules(doc, policy)
+{
+ const meta = doc.createElement("meta");
+ meta.setAttribute("http-equiv", "Content-Security-Policy");
+ meta.setAttribute("content", csp_rule(policy.nonce));
+ doc.head.append(meta);
+ /* CSP is already in effect, we can remove the <meta> now. */
+ meta.remove();
+}
+
+async function sanitize_document(doc, policy)
+{
+ /*
+ * Ensure our CSP rules are employed from the beginning. This CSP injection
+ * method is, when possible, going to be applied together with CSP rules
+ * injected using webRequest.
+ */
+ const has_own_head = doc.head;
+ if (!has_own_head)
+ doc.documentElement.prepend(doc.createElement("head"));
+
+ apply_hachette_csp_rules(doc, policy);
+
+ /* Probably not needed, but...: proceed with DOM in its initial state. */
+ if (!has_own_head)
+ doc.head.remove();
+
+ /*
+ * <html> node gets hijacked now, to be re-attached after <head> is loaded
+ * and sanitized.
+ */
+ const old_html = doc.documentElement;
+ const new_html = doc.createElement("html");
+ old_html.replaceWith(new_html);
+
+ await wait_for_head(doc, old_html);
+
+ for (const meta of old_html.querySelectorAll("head meta"))
+ sanitize_meta(meta, policy);
+
+ new_html.replaceWith(old_html);
+}
+
if (!is_privileged_url(document.URL)) {
const reductor =
(ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
@@ -45,18 +184,10 @@ if (!is_privileged_url(document.URL)) {
if (signature)
document.cookie = `hachette-${signature}=; Max-Age=-1;`;
- handle_page_actions(policy.nonce);
+ if (!policy.allow)
+ sanitize_document(document, policy);
- if (!policy.allow) {
- const old_html = document.documentElement;
- const new_html = document.createElement("html");
- old_html.replaceWith(new_html);
- old_html.hachette_corresponding = new_html;
-
- const modify_end =
- modify_on_the_fly(old_html, policy, {node_eater: accept_node});
- document.addEventListener("DOMContentLoaded", modify_end);
- }
+ handle_page_actions(policy.nonce);
start_activity_info_server();
}