From 692577bbde5e8110855c022ec913324dfddce9ae Mon Sep 17 00:00:00 2001 From: jahoti Date: Fri, 16 Jul 2021 00:00:00 +0000 Subject: Use URL-based policy smuggling Increase the power of URL-based smuggling by making it (effectively) compulsory in all cases and adapting a structure. While the details still need to be worked out, the potential for future expansion is there. --- background/policy_injector.js | 52 ++++++++++++++++++++++++++++++++++++++----- common/misc.js | 36 ++++++++++++++++++++++++++++-- content/main.js | 49 +++++++++++++--------------------------- copyright | 2 +- 4 files changed, 97 insertions(+), 42 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index eb67963..9e8ed61 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -2,6 +2,7 @@ * Myext injecting policy to page using webRequest * * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ @@ -12,7 +13,9 @@ * IMPORT browser * IMPORT is_chrome * IMPORT gen_unique + * IMPORT gen_nonce * IMPORT url_item + * IMPORT url_extract_policy * IMPORT get_query_best * IMPORT csp_rule * IMPORTS_END @@ -39,18 +42,46 @@ function is_our_header(header, rule) return header.value === rule } -function inject(details) +function url_inject(details) { - const url = url_item(details.url); + const targets = url_extract_policy(details.url); + if (targets.policy) { + return; + } else if (targets.signed) { + /* Redirect; update policy */ + targets.target = targets.target2; + delete targets.target2 + } + + let redirect_url = targets.base_url + targets.sig; + let [pattern, settings] = query_best(targets.base_url); + if (!pattern) + /* Defaults */ + settings = {}; + + const policy = {allow: settings.allow, nonce: gen_nonce()}; + + redirect_url += encodeURIComponent(JSON.stringify(policy)); + if (targets.target) + redirect_url += targets.target; + if (targets.target2) + redirect_url += targets.target2; + + return {redirectUrl: redirect_url}; +} - const [pattern, settings] = query_best(url); +function inject(details) +{ + const targets = url_extract_policy(details.url); + if (!targets.policy) + /* Block unsigned requests */ + return {cancel: true}; - const nonce = gen_unique(url); - const rule = csp_rule(nonce); + const rule = csp_rule(targets.policy.nonce); var headers; - if (settings !== undefined && settings.allow) { + if (targets.policy.allow) { /* * Chrome doesn't have the buggy behavior of repeatedly injecting a * header we injected once. Firefox does and we have to remove it there. @@ -80,6 +111,15 @@ async function start_policy_injector() if (is_chrome) extra_opts.push("extraHeaders"); + browser.webRequest.onBeforeRequest.addListener( + url_inject, + { + urls: [""], + types: ["main_frame", "sub_frame"] + }, + ["blocking"] + ); + browser.webRequest.onHeadersReceived.addListener( inject, { diff --git a/common/misc.js b/common/misc.js index 8b56e79..825a117 100644 --- a/common/misc.js +++ b/common/misc.js @@ -2,6 +2,7 @@ * Myext miscellaneous operations refactored to a separate file * * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ @@ -14,6 +15,14 @@ * IMPORTS_END */ +/* Generate a random base64-encoded 128-bit sequence */ +function gen_nonce() +{ + let randomData = new Uint8Array(16); + crypto.getRandomValues(randomData); + return btoa(String.fromCharCode.apply(null, randomData)); +} + /* * generating unique, per-site value that can be computed synchronously * and is impossible to guess for a malicious website @@ -26,9 +35,9 @@ function gen_unique(url) function get_secure_salt() { if (is_chrome) - return browser.runtime.getManifest().key.substring(0, 50); + return browser.runtime.getManifest().key.substring(0, 36); else - return browser.runtime.getURL("dummy"); + return browser.runtime.getURL("dummy").substr(16, 36); } /* @@ -95,11 +104,34 @@ function is_privileged_url(url) return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url); } +/* Extract any policy present in the URL */ +function url_extract_policy(url) +{ + const targets = url_extract_target(url); + const key = '#' + get_secure_salt(); + targets.sig = key + gen_unique(targets.base_url); + + if (targets.target && targets.target.startsWith(key)) { + targets.signed = true; + if (targets.target.startsWith(targets.sig)) + try { + const policy_string = targets.target.substring(101); + targets.policy = JSON.parse(decodeURIComponent(policy_string)); + } catch (e) { + /* TODO what should happen here? */ + } + } + + return targets; +} + /* * EXPORTS_START + * EXPORT gen_nonce * EXPORT gen_unique * EXPORT url_item * EXPORT url_extract_target + * EXPORT url_extract_policy * EXPORT csp_rule * EXPORT nice_name * EXPORT open_in_settings diff --git a/content/main.js b/content/main.js index 9acf749..e75f61d 100644 --- a/content/main.js +++ b/content/main.js @@ -2,6 +2,7 @@ * Myext main content script run in all frames * * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ @@ -10,7 +11,9 @@ * IMPORT handle_page_actions * IMPORT url_item * IMPORT url_extract_target + * IMPORT url_extract_policy * IMPORT gen_unique + * IMPORT gen_nonce * IMPORT csp_rule * IMPORT is_privileged_url * IMPORT sanitize_attributes @@ -32,32 +35,6 @@ * urls has not yet been added to the extension. */ -let url = url_item(document.URL); -let unique = gen_unique(url); - - -function is_http() -{ - return !!/^https?:\/\//i.exec(document.URL); -} - -function is_whitelisted() -{ - const parsed_url = url_extract_target(document.URL); - - if (parsed_url.target !== undefined && - parsed_url.target === '#' + unique) { - if (parsed_url.target2 !== undefined) - window.location.href = parsed_url.base_url + parsed_url.target2; - else - history.replaceState(null, "", parsed_url.base_url); - - return true; - } - - return false; -} - function handle_mutation(mutations, observer) { if (document.readyState === 'complete') { @@ -113,7 +90,7 @@ function inject_csp(head) let meta = document.createElement("meta"); meta.setAttribute("http-equiv", "Content-Security-Policy"); - meta.setAttribute("content", csp_rule(unique)); + meta.setAttribute("content", csp_rule(nonce)); if (head.firstElementChild === null) head.appendChild(meta); @@ -122,14 +99,20 @@ function inject_csp(head) } if (!is_privileged_url(document.URL)) { + const targets = url_extract_policy(document.URL); + targets.policy = targets.policy || {}; + const nonce = targets.policy.nonce || gen_nonce(); + + if (targets.signed) + if (targets.target2 !== undefined) + window.location.href = targets.base_url + targets.target2; + else + history.replaceState(null, "", targets.base_url); + start_activity_info_server(); - handle_page_actions(unique); + handle_page_actions(nonce); - if (is_http()) { - /* rely on CSP injected through webRequest */ - } else if (is_whitelisted()) { - /* do not block scripts at all */ - } else { + if (!targets.policy.allow) { block_nodes_recursively(document.documentElement); if (is_chrome) { diff --git a/copyright b/copyright index a01c1fe..4ff7483 100644 --- a/copyright +++ b/copyright @@ -10,7 +10,7 @@ Files: build.sh Copyright: 2021 Wojtek Kosior License: CC0 -Files: manifest.json +Files: manifest.json background/policy_injector.js common/misc.js content/main.js Copyright: 2021 Wojtek Kosior 2021 jahoti License: GPL-3+-javascript or Alicense-1.0 -- cgit v1.2.3