Project

General

Profile

« Previous | Next » 

Revision 96068ada

Added by koszko almost 2 years ago

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.

View differences:

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