Revision 96068ada
Added by koszko almost 2 years ago
background/main.js | ||
---|---|---|
17 | 17 |
* IMPORT browser |
18 | 18 |
* IMPORT is_privileged_url |
19 | 19 |
* IMPORT query_best |
20 |
* IMPORT gen_nonce |
|
21 | 20 |
* IMPORT inject_csp_headers |
22 | 21 |
* IMPORT apply_stream_filter |
23 |
* IMPORT filter_cookie_headers |
|
24 | 22 |
* IMPORT is_chrome |
23 |
* IMPORT is_mozilla |
|
25 | 24 |
* IMPORTS_END |
26 | 25 |
*/ |
27 | 26 |
|
... | ... | |
51 | 50 |
|
52 | 51 |
browser.runtime.onInstalled.addListener(init_ext); |
53 | 52 |
|
53 |
/* |
|
54 |
* The function below implements a more practical interface for what it does by |
|
55 |
* wrapping the old query_best() function. |
|
56 |
*/ |
|
57 |
function decide_policy_for_url(storage, policy_observable, url) |
|
58 |
{ |
|
59 |
if (storage === undefined) |
|
60 |
return {allow: false}; |
|
61 |
|
|
62 |
const settings = |
|
63 |
{allow: policy_observable !== undefined && policy_observable.value}; |
|
64 |
|
|
65 |
const [pattern, queried_settings] = query_best(storage, url); |
|
66 |
|
|
67 |
if (queried_settings) { |
|
68 |
settings.payload = queried_settings.components; |
|
69 |
settings.allow = !!queried_settings.allow && !settings.payload; |
|
70 |
settings.pattern = pattern; |
|
71 |
} |
|
72 |
|
|
73 |
return settings; |
|
74 |
} |
|
54 | 75 |
|
55 | 76 |
let storage; |
56 | 77 |
let policy_observable = {}; |
57 | 78 |
|
58 |
function on_headers_received(details)
|
|
79 |
function sanitize_web_page(details)
|
|
59 | 80 |
{ |
60 | 81 |
const url = details.url; |
61 | 82 |
if (is_privileged_url(details.url)) |
62 | 83 |
return; |
63 | 84 |
|
64 |
const [pattern, settings] = query_best(storage, details.url); |
|
65 |
const has_payload = !!(settings && settings.components); |
|
66 |
const allow = !has_payload && |
|
67 |
!!(settings ? settings.allow : policy_observable.value); |
|
68 |
const nonce = gen_nonce(); |
|
69 |
const policy = {allow, url, nonce, has_payload}; |
|
85 |
const policy = |
|
86 |
decide_policy_for_url(storage, policy_observable, details.url); |
|
70 | 87 |
|
71 | 88 |
let headers = details.responseHeaders; |
89 |
|
|
90 |
headers = inject_csp_headers(headers, policy); |
|
91 |
|
|
72 | 92 |
let skip = false; |
73 | 93 |
for (const header of headers) { |
74 | 94 |
if ((header.name.toLowerCase().trim() === "content-disposition" && |
75 | 95 |
/^\s*attachment\s*(;.*)$/i.test(header.value))) |
76 | 96 |
skip = true; |
77 | 97 |
} |
78 |
|
|
79 |
headers = inject_csp_headers(headers, policy); |
|
80 |
|
|
81 | 98 |
skip = skip || (details.statusCode >= 300 && details.statusCode < 400); |
99 |
|
|
82 | 100 |
if (!skip) { |
83 | 101 |
/* Check for API availability. */ |
84 | 102 |
if (browser.webRequest.filterResponseData) |
... | ... | |
88 | 106 |
return {responseHeaders: headers}; |
89 | 107 |
} |
90 | 108 |
|
91 |
function on_before_send_headers(details) |
|
109 |
const request_url_regex = /^[^?]*\?url=(.*)$/; |
|
110 |
const redirect_url_template = browser.runtime.getURL("dummy") + "?settings="; |
|
111 |
|
|
112 |
function synchronously_smuggle_policy(details) |
|
92 | 113 |
{ |
93 |
let headers = details.requestHeaders; |
|
94 |
headers = filter_cookie_headers(headers); |
|
95 |
return {requestHeaders: headers}; |
|
114 |
/* |
|
115 |
* Content script will make a synchronous XmlHttpRequest to extension's |
|
116 |
* `dummy` file to query settings for given URL. We smuggle that |
|
117 |
* information in query parameter of the URL we redirect to. |
|
118 |
* A risk of fingerprinting arises if a page with script execution allowed |
|
119 |
* guesses the dummy file URL and makes an AJAX call to it. It is currently |
|
120 |
* a problem in ManifestV2 Chromium-family port of Haketilo because Chromium |
|
121 |
* uses predictable URLs for web-accessible resources. We plan to fix it in |
|
122 |
* the future ManifestV3 port. |
|
123 |
*/ |
|
124 |
if (details.type !== "xmlhttprequest") |
|
125 |
return {cancel: true}; |
|
126 |
|
|
127 |
console.debug(`Settings queried using XHR for '${details.url}'.`); |
|
128 |
|
|
129 |
let policy = {allow: false}; |
|
130 |
|
|
131 |
try { |
|
132 |
/* |
|
133 |
* request_url should be of the following format: |
|
134 |
* <url_for_extension's_dummy_file>?url=<valid_urlencoded_url> |
|
135 |
*/ |
|
136 |
const match = request_url_regex.exec(details.url); |
|
137 |
const queried_url = decodeURIComponent(match[1]); |
|
138 |
|
|
139 |
if (details.initiator && !queried_url.startsWith(details.initiator)) { |
|
140 |
console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`); |
|
141 |
return {cancel: true}; |
|
142 |
} |
|
143 |
|
|
144 |
policy = decide_policy_for_url(storage, policy_observable, queried_url); |
|
145 |
} catch (e) { |
|
146 |
console.warn(`Bad request! Expected ${browser.runtime.getURL("dummy")}?url=<valid_urlencoded_url>. Got ${request_url}. This might be the result of page fingerprinting the browser.`); |
|
147 |
} |
|
148 |
|
|
149 |
const encoded_policy = encodeURIComponent(JSON.stringify(policy)); |
|
150 |
|
|
151 |
return {redirectUrl: redirect_url_template + encoded_policy}; |
|
96 | 152 |
} |
97 | 153 |
|
98 | 154 |
const all_types = [ |
... | ... | |
110 | 166 |
extra_opts.push("extraHeaders"); |
111 | 167 |
|
112 | 168 |
browser.webRequest.onHeadersReceived.addListener( |
113 |
on_headers_received,
|
|
169 |
sanitize_web_page,
|
|
114 | 170 |
{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]}, |
115 | 171 |
extra_opts.concat("responseHeaders") |
116 | 172 |
); |
117 | 173 |
|
118 |
browser.webRequest.onBeforeSendHeaders.addListener( |
|
119 |
on_before_send_headers, |
|
120 |
{urls: ["<all_urls>"], types: all_types}, |
|
121 |
extra_opts.concat("requestHeaders") |
|
174 |
const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*"; |
|
175 |
browser.webRequest.onBeforeRequest.addListener( |
|
176 |
synchronously_smuggle_policy, |
|
177 |
{urls: [dummy_url_pattern], types: ["xmlhttprequest"]}, |
|
178 |
extra_opts |
|
122 | 179 |
); |
123 | 180 |
|
124 | 181 |
policy_observable = await light_storage.observe_var("default_allow"); |
125 | 182 |
} |
126 | 183 |
|
127 | 184 |
start_webRequest_operations(); |
185 |
|
|
186 |
const code = `\ |
|
187 |
console.warn("Hi, I'm Mr Dynamic!"); |
|
188 |
|
|
189 |
console.debug("let's see how window.killtheweb looks like now"); |
|
190 |
|
|
191 |
console.log("killtheweb", window.killtheweb); |
|
192 |
` |
|
193 |
|
|
194 |
async function test_dynamic_content_scripts() |
|
195 |
{ |
|
196 |
browser.contentScripts.register({ |
|
197 |
"js": [{code}], |
|
198 |
"matches": ["<all_urls>"], |
|
199 |
"allFrames": true, |
|
200 |
"runAt": "document_start" |
|
201 |
}); |
|
202 |
} |
|
203 |
|
|
204 |
if (is_mozilla) |
|
205 |
test_dynamic_content_scripts(); |
Also available in: Unified diff
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.