Project

General

Profile

« Previous | Next » 

Revision 44e89d8e

Added by koszko almost 2 years ago

simplify CSP handling

All page's CSP rules are now removed when a payload is to be injected. When there is no payload, CSP rules are not modified but only supplemented with Hachette's own.

View differences:

background/policy_injector.js
10 10
 * IMPORTS_START
11 11
 * IMPORT sign_data
12 12
 * IMPORT extract_signed
13
 * IMPORT sanitize_csp_header
14
 * IMPORT csp_rule
15
 * IMPORT is_csp_header_name
13
 * IMPORT make_csp_rule
14
 * IMPORT csp_header_regex
16 15
 * IMPORTS_END
17 16
 */
18 17

  
......
43 42
	break;
44 43
    }
45 44

  
45
    if (policy.has_payload) {
46
	csp_headers = [];
47
	const non_csp_headers = [];
48
	const header_list =
49
	      h => csp_header_regex.test(h) ? csp_headers : non_csp_headers;
50
	headers.forEach(h => header_list(h.name).push(h));
51
	headers = non_csp_headers;
52
    } else {
53
	headers.push(...csp_headers || []);
54
    }
55

  
46 56
    if (!hachette_header) {
47 57
	hachette_header = {name: "x-hachette"};
48 58
	headers.push(hachette_header);
49 59
    }
50 60

  
51
    csp_headers = csp_headers ||
52
	headers.filter(h => is_csp_header_name(h.name));
53

  
54
    /* When blocking remove report-only CSP headers that snitch on us. */
55
    headers = headers.filter(h => !is_csp_header_name(h.name, !policy.allow));
56

  
57 61
    if (old_signature)
58 62
	headers = headers.filter(h => h.value.search(old_signature) === -1);
59 63

  
60
    headers.push(...csp_headers.map(h => sanitize_csp_header(h, policy)));
61

  
62 64
    const policy_str = encodeURIComponent(JSON.stringify(policy));
63 65
    const signed_policy = sign_data(policy_str, new Date().getTime());
64 66
    const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();
......
76 78
    hachette_data = encodeURIComponent(JSON.stringify(hachette_data));
77 79
    hachette_header.value = sign_data(hachette_data, 0).join("_");
78 80

  
79
    /* To ensure there is a CSP header if required */
80
    if (!policy.allow)
81
    if (!policy.allow) {
81 82
	headers.push({
82 83
	    name: "content-security-policy",
83
	    value: csp_rule(policy.nonce)
84
	    value: make_csp_rule(policy)
84 85
	});
86
    }
85 87

  
86 88
    return headers;
87 89
}
background/stream_filter.js
12 12
/*
13 13
 * IMPORTS_START
14 14
 * IMPORT browser
15
 * IMPORT is_csp_header_name
15
 * IMPORT csp_header_regex
16 16
 * IMPORTS_END
17 17
 */
18 18

  
......
116 116
    const doc = new DOMParser().parseFromString(html, "text/html");
117 117

  
118 118
    for (const meta of doc.querySelectorAll("head>meta[http-equiv]")) {
119
	if (is_csp_header_name(meta.getAttribute("http-equiv"), true) &&
120
	    meta.content)
119
	if (csp_header_regex.test(meta.httpEquiv) && meta.content)
121 120
	    return true;
122 121
    }
123 122

  
common/misc.js
43 43
    return Uint8toHex(randomData);
44 44
}
45 45

  
46
/* csp rule that blocks all scripts except for those injected by us */
47
function csp_rule(nonce)
46
/* CSP rule that blocks scripts according to policy's needs. */
47
function make_csp_rule(policy)
48 48
{
49
    const rule = `'nonce-${nonce}'`;
50
    return `script-src ${rule}; script-src-elem ${rule}; script-src-attr 'none'; prefetch-src 'none';`;
49
    let rule = "prefetch-src 'none'; script-src-attr 'none';";
50
    const script_src = policy.has_payload ?
51
	  `'nonce-${policy.nonce}'` : "'none'";
52
    rule += ` script-src ${script_src}; script-src-elem ${script_src};`;
53
    return rule;
51 54
}
52 55

  
53 56
/* Check if some HTTP header might define CSP rules. */
54
const csp_header_names = new Set([
55
    "content-security-policy",
56
    "x-webkit-csp",
57
    "x-content-security-policy"
58
]);
59

  
60
const report_only_header_name = "content-security-policy-report-only";
61

  
62
function is_csp_header_name(string, include_report_only)
63
{
64
    string = string && string.toLowerCase().trim() || "";
65

  
66
    return (include_report_only && string === report_only_header_name) ||
67
	csp_header_names.has(string);
68
}
57
const csp_header_regex =
58
      /^\s*(content-security-policy|x-webkit-csp|x-content-security-policy)/i;
69 59

  
70 60
/*
71 61
 * Print item together with type, e.g.
......
111 101
    return directives;
112 102
}
113 103

  
114
/* Make CSP headers do our bidding, not interfere */
115
function sanitize_csp_header(header, policy)
116
{
117
    const rule = `'nonce-${policy.nonce}'`;
118
    const csp = parse_csp(header.value);
119

  
120
    if (!policy.allow) {
121
	/* No snitching */
122
	delete csp['report-to'];
123
	delete csp['report-uri'];
124

  
125
	delete csp['script-src'];
126
	delete csp['script-src-elem'];
127

  
128
	csp['script-src-attr'] = ["'none'"];
129
	csp['prefetch-src'] = ["'none'"];
130
    }
131

  
132
    if ('script-src' in csp)
133
	csp['script-src'].push(rule);
134
    else
135
	csp['script-src'] = [rule];
136

  
137
    if ('script-src-elem' in csp)
138
	csp['script-src-elem'].push(rule);
139
    else
140
	csp['script-src-elem'] = [rule];
141

  
142
    const new_csp = Object.entries(csp).map(
143
	i => `${i[0]} ${i[1].join(' ')};`
144
    );
145

  
146
    return {name: header.name, value: new_csp.join('')};
147
}
148

  
149 104
/* Regexes and objects to use as/in schemas for parse_json_with_schema(). */
150 105
const nonempty_string_matcher = /.+/;
151 106

  
......
161 116
/*
162 117
 * EXPORTS_START
163 118
 * EXPORT gen_nonce
164
 * EXPORT csp_rule
165
 * EXPORT is_csp_header_name
119
 * EXPORT make_csp_rule
120
 * EXPORT csp_header_regex
166 121
 * EXPORT nice_name
167 122
 * EXPORT open_in_settings
168 123
 * EXPORT is_privileged_url
169
 * EXPORT sanitize_csp_header
170 124
 * EXPORT matchers
171 125
 * EXPORTS_END
172 126
 */
content/main.js
16 16
 * IMPORT is_chrome
17 17
 * IMPORT is_mozilla
18 18
 * IMPORT start_activity_info_server
19
 * IMPORT csp_rule
20
 * IMPORT is_csp_header_name
21
 * IMPORT sanitize_csp_header
19
 * IMPORT make_csp_rule
20
 * IMPORT csp_header_regex
22 21
 * IMPORTS_END
23 22
 */
24 23

  
......
172 171
	const name = construct_name.join("");
173 172
	seta(node, `${blocked_str}-${name}`, geta(node, name));
174 173
    }
175
}
176

  
177
function sanitize_meta(meta, policy)
178
{
179
    const value = meta.content || "";
180 174

  
181
    if (!value || !is_csp_header_name(meta.httpEquiv || "", true))
182
	return;
183

  
184
    block_attribute(meta, "content");
175
    rema(node, attr);
185 176
}
186 177

  
187 178
/*
188
 * Used to disable <script> that has not yet been added to live DOM (doesn't
189
 * work for those already added).
179
 * Used to disable `<script>'s and `<meta>'s that have not yet been added to
180
 * live DOM (doesn't work for those already added).
190 181
 */
182
function sanitize_meta(meta)
183
{
184
    if (csp_header_regex.test(meta.httpEquiv) && meta.content)
185
	block_attribute(meta, "content");
186
}
187

  
191 188
function sanitize_script(script)
192 189
{
193 190
    script.hachette_blocked_type = script.getAttribute("type");
......
195 192
}
196 193

  
197 194
/*
198
 * Executed after script has been connected to the DOM, when it is no longer
199
 * eligible for being executed by the browser
195
 * Executed after `<script>' has been connected to the DOM, when it is no longer
196
 * eligible for being executed by the browser.
200 197
 */
201
function desanitize_script(script, policy)
198
function desanitize_script(script)
202 199
{
203 200
    script.setAttribute("type", script.hachette_blocked_type);
204 201

  
205
    if (script.hachette_blocked_type === null)
202
    if ([null, undefined].includes(script.hachette_blocked_type))
206 203
	script.removeAttribute("type");
207 204

  
208 205
    delete script.hachette_blocked_type;
......
233 230
 * cause part of the DOM to be loaded when our content scripts get to run. Thus,
234 231
 * before the CSP rules we inject (for non-HTTP pages) become effective, we need
235 232
 * to somehow block the execution of `<script>'s and intrinsics that were
236
 * already there.
233
 * already there. Additionally, some browsers (IceCat 60) seem to have problems
234
 * applying this CSP to non-inline `<scripts>' in certain scenarios.
237 235
 */
236
function prevent_script_execution(event)
237
{
238
    if (!event.target._hachette_payload)
239
	event.preventDefault();
240
}
241

  
238 242
function mozilla_initial_block(doc)
239 243
{
240
    const blocker = e => e.preventDefault();
241
    doc.addEventListener("beforescriptexecute", blocker);
242
    setTimeout(() => doc.removeEventListener("beforescriptexecute", blocker));
244
    doc.addEventListener("beforescriptexecute", prevent_script_execution);
243 245

  
244 246
    [...doc.all].flatMap(ele => [...ele.attributes].map(attr => [ele, attr]))
245 247
	.map(([ele, attr]) => [ele, attr.localName])
......
273 275
     * non-HTML documents.
274 276
     */
275 277
    const html = new DOMParser().parseFromString(`<html><head><meta \
276
http-equiv="Content-Security-Policy" content="${csp_rule(policy.nonce)}"\
278
http-equiv="Content-Security-Policy" content="${make_csp_rule(policy)}"\
277 279
/></head><body>Loading...</body></html>`, "text/html").documentElement;
278 280

  
279 281
    /*
......
284 286
    root.replaceWith(html);
285 287

  
286 288
    /*
287
     * For XML documents, we don't intend to inject payload, so we neither block
288
     * document's CSP `<meta>' tags nor wait for `<head>' to be parsed.
289
     * When we don't inject payload, we neither block document's CSP `<meta>'
290
     * tags nor wait for `<head>' to be parsed.
289 291
     */
290
    if (document instanceof HTMLDocument) {
292
    if (policy.has_payload) {
291 293
	await wait_for_head(doc, root);
292 294

  
293 295
	root.querySelectorAll("head meta")
......
333 335
	policy = {allow: false, nonce: gen_nonce()};
334 336
    }
335 337

  
338
    if (!(document instanceof HTMLDocument))
339
	policy.has_payload = false;
340

  
336 341
    console.debug("current policy", policy);
337 342

  
338 343
    const doc_ready = Promise.all([

Also available in: Unified diff