From 96068ada37bfa1d7e6485551138ba36600664caf Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Sat, 20 Nov 2021 18:29:59 +0100 Subject: replace cookies with synchronous XmlHttpRequest as policy smuggling method. Note: this breaks Mozilla port of Haketilo. Synchronous XmlHttpRequest doesn't work as well there. This will be fixed with dynamically-registered content scripts later. --- background/main.js | 120 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 21 deletions(-) (limited to 'background/main.js') diff --git a/background/main.js b/background/main.js index 40b3a9e..9cdfb97 100644 --- a/background/main.js +++ b/background/main.js @@ -17,11 +17,10 @@ * IMPORT browser * IMPORT is_privileged_url * IMPORT query_best - * IMPORT gen_nonce * IMPORT inject_csp_headers * IMPORT apply_stream_filter - * IMPORT filter_cookie_headers * IMPORT is_chrome + * IMPORT is_mozilla * IMPORTS_END */ @@ -51,34 +50,53 @@ async function init_ext(install_details) browser.runtime.onInstalled.addListener(init_ext); +/* + * The function below implements a more practical interface for what it does by + * wrapping the old query_best() function. + */ +function decide_policy_for_url(storage, policy_observable, url) +{ + if (storage === undefined) + return {allow: false}; + + const settings = + {allow: policy_observable !== undefined && policy_observable.value}; + + const [pattern, queried_settings] = query_best(storage, url); + + if (queried_settings) { + settings.payload = queried_settings.components; + settings.allow = !!queried_settings.allow && !settings.payload; + settings.pattern = pattern; + } + + return settings; +} let storage; let policy_observable = {}; -function on_headers_received(details) +function sanitize_web_page(details) { const url = details.url; if (is_privileged_url(details.url)) return; - const [pattern, settings] = query_best(storage, details.url); - const has_payload = !!(settings && settings.components); - const allow = !has_payload && - !!(settings ? settings.allow : policy_observable.value); - const nonce = gen_nonce(); - const policy = {allow, url, nonce, has_payload}; + const policy = + decide_policy_for_url(storage, policy_observable, details.url); let headers = details.responseHeaders; + + headers = inject_csp_headers(headers, policy); + let skip = false; for (const header of headers) { if ((header.name.toLowerCase().trim() === "content-disposition" && /^\s*attachment\s*(;.*)$/i.test(header.value))) skip = true; } - - headers = inject_csp_headers(headers, policy); - skip = skip || (details.statusCode >= 300 && details.statusCode < 400); + if (!skip) { /* Check for API availability. */ if (browser.webRequest.filterResponseData) @@ -88,11 +106,49 @@ function on_headers_received(details) return {responseHeaders: headers}; } -function on_before_send_headers(details) +const request_url_regex = /^[^?]*\?url=(.*)$/; +const redirect_url_template = browser.runtime.getURL("dummy") + "?settings="; + +function synchronously_smuggle_policy(details) { - let headers = details.requestHeaders; - headers = filter_cookie_headers(headers); - return {requestHeaders: headers}; + /* + * Content script will make a synchronous XmlHttpRequest to extension's + * `dummy` file to query settings for given URL. We smuggle that + * information in query parameter of the URL we redirect to. + * A risk of fingerprinting arises if a page with script execution allowed + * guesses the dummy file URL and makes an AJAX call to it. It is currently + * a problem in ManifestV2 Chromium-family port of Haketilo because Chromium + * uses predictable URLs for web-accessible resources. We plan to fix it in + * the future ManifestV3 port. + */ + if (details.type !== "xmlhttprequest") + return {cancel: true}; + + console.debug(`Settings queried using XHR for '${details.url}'.`); + + let policy = {allow: false}; + + try { + /* + * request_url should be of the following format: + * ?url= + */ + const match = request_url_regex.exec(details.url); + const queried_url = decodeURIComponent(match[1]); + + if (details.initiator && !queried_url.startsWith(details.initiator)) { + console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`); + return {cancel: true}; + } + + policy = decide_policy_for_url(storage, policy_observable, queried_url); + } catch (e) { + console.warn(`Bad request! Expected ${browser.runtime.getURL("dummy")}?url=. Got ${request_url}. This might be the result of page fingerprinting the browser.`); + } + + const encoded_policy = encodeURIComponent(JSON.stringify(policy)); + + return {redirectUrl: redirect_url_template + encoded_policy}; } const all_types = [ @@ -110,18 +166,40 @@ async function start_webRequest_operations() extra_opts.push("extraHeaders"); browser.webRequest.onHeadersReceived.addListener( - on_headers_received, + sanitize_web_page, {urls: [""], types: ["main_frame", "sub_frame"]}, extra_opts.concat("responseHeaders") ); - browser.webRequest.onBeforeSendHeaders.addListener( - on_before_send_headers, - {urls: [""], types: all_types}, - extra_opts.concat("requestHeaders") + const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*"; + browser.webRequest.onBeforeRequest.addListener( + synchronously_smuggle_policy, + {urls: [dummy_url_pattern], types: ["xmlhttprequest"]}, + extra_opts ); policy_observable = await light_storage.observe_var("default_allow"); } start_webRequest_operations(); + +const code = `\ +console.warn("Hi, I'm Mr Dynamic!"); + +console.debug("let's see how window.killtheweb looks like now"); + +console.log("killtheweb", window.killtheweb); +` + +async function test_dynamic_content_scripts() +{ + browser.contentScripts.register({ + "js": [{code}], + "matches": [""], + "allFrames": true, + "runAt": "document_start" +}); +} + +if (is_mozilla) + test_dynamic_content_scripts(); -- cgit v1.2.3