Project

General

Profile

« Previous | Next » 

Revision 014f2a2f

Added by koszko about 2 years ago

implement smuggling via cookies instead of URL

View differences:

background/policy_injector.js
8 8

  
9 9
/*
10 10
 * IMPORTS_START
11
 * IMPORT TYPE_PREFIX
12 11
 * IMPORT get_storage
13 12
 * IMPORT browser
14 13
 * IMPORT is_chrome
15
 * IMPORT is_mozilla
16
 * IMPORT gen_unique
17 14
 * IMPORT gen_nonce
18 15
 * IMPORT is_privileged_url
19
 * IMPORT url_item
20
 * IMPORT url_extract_target
21
 * IMPORT sign_policy
16
 * IMPORT sign_data
17
 * IMPORT extract_signed
22 18
 * IMPORT query_best
23 19
 * IMPORT sanitize_csp_header
20
 * IMPORT csp_rule
24 21
 * IMPORTS_END
25 22
 */
26 23

  
......
32 29
    "x-content-security-policy"
33 30
]);
34 31

  
35
/* TODO: variable no longer in use; remove if not needed */
36
const unwanted_csp_directives = new Set([
37
    "report-to",
38
    "report-uri",
39
    "script-src",
40
    "script-src-elem",
41
    "prefetch-src"
42
]);
43

  
44 32
const report_only = "content-security-policy-report-only";
45 33

  
46
function url_inject(details)
34
function headers_inject(details)
47 35
{
48
    if (is_privileged_url(details.url))
36
    console.log("ijnector details", details);
37
    const url = details.url;
38
    if (is_privileged_url(url))
49 39
	return;
50 40

  
51
    const targets = url_extract_target(details.url);
52
    if (targets.current)
53
	return;
41
    const [pattern, settings] = query_best(storage, url);
42
    const allow = !!(settings && settings.allow);
43
    const nonce = gen_nonce();
44
    const rule = `'nonce-${nonce}'`;
54 45

  
55
    /* Redirect; update policy */
56
    if (targets.policy)
57
	targets.target = "";
58

  
59
    let [pattern, settings] = query_best(storage, targets.base_url);
60
    /* Defaults */
61
    if (!pattern)
62
	settings = {};
63

  
64
    const policy = encodeURIComponent(
65
	JSON.stringify({
66
	    allow: settings.allow,
67
	    nonce: gen_nonce(),
68
	    base_url: targets.base_url
69
	})
70
    );
46
    let orig_csp_headers;
47
    let old_signature;
48
    let hachette_header;
49
    let headers = details.responseHeaders;
71 50

  
72
    return {
73
	redirectUrl: [
74
	    targets.base_url,
75
	    '#', sign_policy(policy, new Date()), policy,
76
	    targets.target,
77
	    targets.target2
78
	].join("")
79
    };
80
}
51
    for (const header of headers.filter(h => h.name === "x-hachette")) {
52
	const match = /^([^%])(%.*)$/.exec(header.value);
53
	if (!match)
54
	    continue;
81 55

  
82
function headers_inject(details)
83
{
84
    const targets = url_extract_target(details.url);
85
    /* Block mis-/unsigned requests */
86
    if (!targets.current)
87
	return {cancel: true};
88

  
89
    let orig_csp_headers = is_chrome ? null : [];
90
    let headers = [];
91
    let csp_headers = is_chrome ? headers : [];
92

  
93
    const rule = `'nonce-${targets.policy.nonce}'`;
94
    const block = !targets.policy.allow;
95

  
96
    for (const header of details.responseHeaders) {
97
	if (!csp_header_names.has(header)) {
98
	    /* Remove headers that only snitch on us */
99
	    if (header.name.toLowerCase() === report_only && block)
100
		continue;
101
	    headers.push(header);
102

  
103
	    /* If these are the original CSP headers, use them instead */
104
	    /* Test based on url_extract_target() in misc.js */
105
	    if (is_mozilla && header.name === "x-orig-csp") {
106
		let index = header.value.indexOf('%5B');
107
		if (index === -1)
108
		    continue;
109

  
110
		let sig = header.value.substring(0, index);
111
		let data = header.value.substring(index);
112
		if (sig !== sign_policy(data, 0))
113
		    continue;
114

  
115
		/* Confirmed- it's the originals, smuggled in! */
116
		try {
117
		    data = JSON.parse(decodeURIComponent(data));
118
		} catch (e) {
119
		    /* This should not be reached -
120
			it's our self-produced valid JSON. */
121
		    console.log("Unexpected internal error - invalid JSON smuggled!", e);
122
		}
123

  
124
		orig_csp_headers = csp_headers = null;
125
		for (const header of data)
126
		    headers.push(sanitize_csp_header(header, rule, block));
127
	    }
128
	} else if (is_chrome || !orig_csp_headers) {
129
	    csp_headers.push(sanitize_csp_header(header, rule, block));
130
	    if (is_mozilla)
131
		orig_csp_headers.push(header);
132
	}
56
	const old_data = extract_signed(...match.splice(1, 2), [[0]]);
57
	if (!old_data || old_data.url !== url)
58
	    continue;
59

  
60
	/* Confirmed- it's the originals, smuggled in! */
61
	orig_csp_headers = old_data.csp_headers;
62
	old_signature = old_data.policy_signature;
63

  
64
	hachette_header = header;
65
	break;
133 66
    }
134 67

  
135
    if (orig_csp_headers) {
136
	/** Smuggle in the original CSP headers for future use.
137
	  * These are signed with a time of 0, as it's not clear there
138
	  * is a limit on how long Firefox might retain these headers in
139
	  * the cache.
140
	  */
141
	orig_csp_headers = encodeURIComponent(JSON.stringify(orig_csp_headers));
142
	headers.push({
143
	    name: "x-orig-csp",
144
	    value: sign_policy(orig_csp_headers, 0) + orig_csp_headers
145
	});
146

  
147
	headers = headers.concat(csp_headers);
68
    if (!hachette_header) {
69
	hachette_header = {name: "x-hachette"};
70
	headers.push(hachette_header);
148 71
    }
149 72

  
73
    orig_csp_headers ||=
74
	headers.filter(h => csp_header_names.has(h.name.toLowerCase()));
75
    headers = headers.filter(h => !csp_header_names.has(h.name.toLowerCase()));
76

  
77
    /* Remove headers that only snitch on us */
78
    if (!allow)
79
	headers = headers.filter(h => h.name.toLowerCase() !== report_only);
80

  
81
    if (old_signature)
82
	headers = headers.filter(h => h.name.search(old_signature) === -1);
83

  
84
    const sanitizer = h => sanitize_csp_header(h, rule, allow);
85
    headers.push(...orig_csp_headers.map(sanitizer));
86

  
87
    const policy = encodeURIComponent(JSON.stringify({allow, nonce, url}));
88
    const policy_signature = sign_data(policy, new Date());
89
    const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();
90
    headers.push({
91
	name: "Set-Cookie",
92
	value: `hachette-${policy_signature}=${policy}; Expires=${later_30sec};`
93
    });
94

  
95
    /*
96
     * Smuggle in the signature and the original CSP headers for future use.
97
     * These are signed with a time of 0, as it's not clear there is a limit on
98
     * how long Firefox might retain headers in the cache.
99
     */
100
    let hachette_data = {csp_headers: orig_csp_headers, policy_signature, url};
101
    hachette_data = encodeURIComponent(JSON.stringify(hachette_data));
102
    hachette_header.value = sign_data(hachette_data, 0) + hachette_data;
103

  
150 104
    /* To ensure there is a CSP header if required */
151
    if (block) {
152
	headers.push({
153
	    name: "content-security-policy",
154
	    value: `script-src ${rule}; script-src-elem ${rule}; ` +
155
		"script-src-attr 'none'; prefetch-src 'none';"
156
	});
157
    }
105
    if (!allow)
106
	headers.push({name: "content-security-policy", value: csp_rule(nonce)});
158 107

  
159 108
    return {responseHeaders: headers};
160 109
}
......
167 116
    if (is_chrome)
168 117
	extra_opts.push("extraHeaders");
169 118

  
170
    browser.webRequest.onBeforeRequest.addListener(
171
	url_inject,
172
	{
173
	    urls: ["<all_urls>"],
174
	    types: ["main_frame", "sub_frame"]
175
	},
176
	["blocking"]
177
    );
178

  
179 119
    browser.webRequest.onHeadersReceived.addListener(
180 120
	headers_inject,
181 121
	{
common/misc.js
45 45
    return Uint8toHex(randomData);
46 46
}
47 47

  
48
function gen_unique(url)
49
{
50
    return sha256(get_secure_salt() + url);
51
}
52

  
53 48
function get_secure_salt()
54 49
{
55 50
    if (is_chrome)
......
58 53
	return browser.runtime.getURL("dummy");
59 54
}
60 55

  
61
/*
62
 * stripping url from query and target (everything after `#' or `?'
63
 * gets removed)
64
 */
65
function url_item(url)
66
{
67
    let url_re = /^([^?#]*).*$/;
68
    let match = url_re.exec(url);
69
    return match[1];
70
}
71

  
72
/*
73
 * Assume a url like:
74
 *     https://example.com/green?illuminati=confirmed#<injected-policy>#winky
75
 * This function will make it into an object like:
76
 * {
77
 *     "base_url": "https://example.com/green?illuminati=confirmed",
78
 *     "target":   "#<injected-policy>",
79
 *     "target2":  "#winky",
80
 *     "policy":   <injected-policy-as-js-object>,
81
 *     "current":  <boolean-indicating-whether-policy-url-matches>
82
 * }
83
 * In case url doesn't have 2 #'s, target2 and target can be set to undefined.
84
 */
85
function url_extract_target(url)
56
function extract_signed(signature, data, times)
86 57
{
87
    const url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/;
88
    const match = url_re.exec(url);
89
    const targets  = {
90
	base_url: match[1],
91
	target:   match[3] || "",
92
	target2:  match[4] || ""
93
    };
94
    if (!targets.target)
95
	return targets;
96

  
97
    /* %7B -> { */
98
    const index = targets.target.indexOf('%7B');
99
    if (index === -1)
100
	return targets;
101

  
102 58
    const now = new Date();
103
    const sig = targets.target.substring(1, index);
104
    const policy = targets.target.substring(index);
105
    if (sig !== sign_policy(policy, now) &&
106
	sig !== sign_policy(policy, now, -1))
107
	return targets;
59
    times ||= [[now], [now, -1]];
60

  
61
    const reductor =
62
	  (ok, time) => ok || signature === sign_data(data, ...time);
63
    if (!times.reduce(reductor, false))
64
	return undefined;
108 65

  
109 66
    try {
110
	targets.policy = JSON.parse(decodeURIComponent(policy));
111
	targets.current = targets.policy.base_url === targets.base_url;
67
	return JSON.parse(decodeURIComponent(data));
112 68
    } catch (e) {
113 69
	/* This should not be reached - it's our self-produced valid JSON. */
114 70
	console.log("Unexpected internal error - invalid JSON smuggled!", e);
115 71
    }
116

  
117
    return targets;
118 72
}
119 73

  
120 74
/* csp rule that blocks all scripts except for those injected by us */
121 75
function csp_rule(nonce)
122 76
{
123
    let rule = `script-src 'nonce-${nonce}';`;
124
    if (is_chrome)
125
	rule += `script-src-elem 'nonce-${nonce}';`;
126
    return rule;
77
    const rule = `'nonce-${nonce}'`;
78
    return `script-src ${rule}; script-src-elem ${rule}; script-src-attr 'none'; prefetch-src 'none';`;
127 79
}
128 80

  
129 81
/*
......
149 101
    return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url);
150 102
}
151 103

  
152
/* Sign a given policy for a given time */
153
function sign_policy(policy, now, hours_offset) {
104
/* Sign a given string for a given time */
105
function sign_data(data, now, hours_offset) {
154 106
    let time = Math.floor(now / 3600000) + (hours_offset || 0);
155
    return gen_unique(time + policy);
107
    return sha256(get_secure_salt() + time + data);
156 108
}
157 109

  
158 110
/* Parse a CSP header */
......
175 127
}
176 128

  
177 129
/* Make CSP headers do our bidding, not interfere */
178
function sanitize_csp_header(header, rule, block)
130
function sanitize_csp_header(header, rule, allow)
179 131
{
180 132
    const csp = parse_csp(header.value);
181 133

  
182
    if (block) {
134
    if (!allow) {
183 135
	/* No snitching */
184 136
	delete csp['report-to'];
185 137
	delete csp['report-uri'];
......
223 175
/*
224 176
 * EXPORTS_START
225 177
 * EXPORT gen_nonce
226
 * EXPORT gen_unique
227
 * EXPORT url_item
228
 * EXPORT url_extract_target
229
 * EXPORT sign_policy
178
 * EXPORT extract_signed
179
 * EXPORT sign_data
230 180
 * EXPORT csp_rule
231 181
 * EXPORT nice_name
232 182
 * EXPORT open_in_settings
content/main.js
9 9
/*
10 10
 * IMPORTS_START
11 11
 * IMPORT handle_page_actions
12
 * IMPORT url_extract_target
13
 * IMPORT gen_unique
12
 * IMPORT extract_signed
14 13
 * IMPORT gen_nonce
15 14
 * IMPORT csp_rule
16 15
 * IMPORT is_privileged_url
......
98 97
}
99 98

  
100 99
if (!is_privileged_url(document.URL)) {
101
    const targets = url_extract_target(document.URL);
102
    if (targets.policy) {
103
	if (targets.target2)
104
	    window.location.href = targets.base_url + targets.target2;
105
	else
106
	    history.replaceState(null, "", targets.base_url);
100
    const reductor =
101
	  (ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
102
    const matches = [...document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)];
103
    let [policy, signature] = matches.reduce(reductor, []);
104

  
105
    console.log("extracted policy", [signature, policy]);
106
    if (!policy || policy.url !== document.URL) {
107
	console.log("using default policy");
108
	policy = {allow: false, nonce: gen_nonce()};
107 109
    }
108 110

  
109
    const policy = targets.current ? targets.policy : {};
111
    if (signature)
112
	document.cookie = `hachette-${signature}=; Max-Age=-1;`;
110 113

  
111
    nonce = policy.nonce || gen_nonce();
112
    handle_page_actions(nonce);
114
    handle_page_actions(policy.nonce);
113 115

  
114 116
    if (!policy.allow) {
115 117
	block_nodes_recursively(document.documentElement);
html/display-panel.js
16 16
 * IMPORT get_import_frame
17 17
 * IMPORT query_all
18 18
 * IMPORT CONNECTION_TYPE
19
 * IMPORT url_item
20 19
 * IMPORT is_privileged_url
21 20
 * IMPORT TYPE_PREFIX
22 21
 * IMPORT nice_name
......
60 59
	return;
61 60
    }
62 61

  
63
    tab_url = url_item(tab.url);
62
    tab_url = /^([^?#]*)/.exec(tab.url)[1];
64 63
    page_url_heading.textContent = tab_url;
65 64
    if (is_privileged_url(tab_url)) {
66 65
	show_privileged_notice_chbx.checked = true;

Also available in: Unified diff