diff options
Diffstat (limited to 'content/main.js')
-rw-r--r-- | content/main.js | 155 |
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(); } |