Project

General

Profile

Download (5.21 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / background / policy_injector.js @ 5fcc9808

1
/**
2
 * Hachette injecting policy to page using webRequest
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Copyright (C) 2021 jahoti
6
 * Redistribution terms are gathered in the `copyright' file.
7
 */
8

    
9
/*
10
 * IMPORTS_START
11
 * IMPORT TYPE_PREFIX
12
 * IMPORT get_storage
13
 * IMPORT browser
14
 * IMPORT is_chrome
15
 * IMPORT is_mozilla
16
 * IMPORT gen_unique
17
 * IMPORT gen_nonce
18
 * IMPORT is_privileged_url
19
 * IMPORT url_extract_target
20
 * IMPORT sign_policy
21
 * IMPORT get_query_best
22
 * IMPORT parse_csp
23
 * IMPORTS_END
24
 */
25

    
26
var storage;
27
var query_best;
28

    
29
const csp_header_names = new Set([
30
    "content-security-policy",
31
    "x-webkit-csp",
32
    "x-content-security-policy"
33
]);
34

    
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
const report_only = "content-security-policy-report-only";
45

    
46
function url_inject(details)
47
{
48
    if (is_privileged_url(details.url))
49
	return;
50

    
51
    const targets = url_extract_target(details.url);
52
    if (targets.current)
53
	return;
54

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

    
59
    let [pattern, settings] = query_best(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
    );
71

    
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
}
81

    
82
function process_csp_header(header, rule, block)
83
{
84
    const csp = parse_csp(header.value);
85

    
86
    /* No snitching */
87
    delete csp['report-to'];
88
    delete csp['report-uri'];
89

    
90
    if (block) {
91
	delete csp['script-src'];
92
	delete csp['script-src-elem'];
93
	csp['script-src-attr'] = ["'none'"];
94
	csp['prefetch-src'] = ["'none'"];
95
    }
96

    
97
    if ('script-src' in csp)
98
	csp['script-src'].push(rule);
99
    else
100
	csp['script-src'] = [rule];
101

    
102
    if ('script-src-elem' in csp)
103
	csp['script-src-elem'].push(rule);
104
    else
105
	csp['script-src-elem'] = [rule];
106

    
107
    const new_policy = Object.entries(csp).map(
108
	i => `${i[0]} ${i[1].join(' ')};`
109
    );
110

    
111
    return {name: header.name, value: new_policy.join('')};
112
}
113

    
114
function headers_inject(details)
115
{
116
    const targets = url_extract_target(details.url);
117
    /* Block mis-/unsigned requests */
118
    if (!targets.current)
119
	return {cancel: true};
120

    
121
    let orig_csp_headers = is_chrome ? null : [];
122
    let headers = [];
123
    let csp_headers = is_chrome ? headers : [];
124

    
125
    const rule = `'nonce-${targets.policy.nonce}'`;
126
    const block = !targets.policy.allow;
127

    
128
    for (const header of details.responseHeaders) {
129
	if (!csp_header_names.has(header)) {
130
	    /* Retain all non-snitching headers */
131
	    if (header.name.toLowerCase() !== report_only) {
132
		headers.push(header);
133

    
134
		/* If these are the original CSP headers, use them instead */
135
		/* Test based on url_extract_target() in misc.js */
136
		if (is_mozilla && header.name === "x-orig-csp") {
137
		    let index = header.value.indexOf('%5B');
138
		    if (index === -1)
139
			continue;
140

    
141
		    let sig = header.value.substring(0, index);
142
		    let data = header.value.substring(index);
143
		    if (sig !== sign_policy(data, 0))
144
			continue;
145

    
146
		    /* Confirmed- it's the originals, smuggled in! */
147
		    try {
148
			data = JSON.parse(decodeURIComponent(data));
149
		    } catch (e) {
150
			/* This should not be reached -
151
			   it's our self-produced valid JSON. */
152
			console.log("Unexpected internal error - invalid JSON smuggled!", e);
153
		    }
154

    
155
		    orig_csp_headers = csp_headers = null;
156
		    for (const header of data)
157
			headers.push(process_csp_header(header, rule, block));
158
		}
159
	    }
160

    
161
	    continue;
162
	}
163
	if (is_mozilla && !orig_csp_headers)
164
	    continue;
165

    
166
	csp_headers.push(process_csp_header(header, rule, block));
167
	if (is_mozilla)
168
	    orig_csp_headers.push(header);
169
    }
170

    
171
    if (orig_csp_headers) {
172
	/** Smuggle in the original CSP headers for future use.
173
	  * These are signed with a time of 0, as it's not clear there
174
	  * is a limit on how long Firefox might retain these headers in
175
	  * the cache.
176
	  */
177
	orig_csp_headers = encodeURIComponent(JSON.stringify(orig_csp_headers));
178
	headers.push({
179
	    name: "x-orig-csp",
180
	    value: sign_policy(orig_csp_headers, 0) + orig_csp_headers
181
	});
182

    
183
	headers = headers.concat(csp_headers);
184
    }
185

    
186
    /* To ensure there is a CSP header if required */
187
    if (block) {
188
	headers.push({
189
	    name: "content-security-policy",
190
	    value: `script-src ${rule}; script-src-elem ${rule}; ` +
191
		"script-src-attr 'none'; prefetch-src 'none';"
192
	});
193
    }
194

    
195
    return {responseHeaders: headers};
196
}
197

    
198
async function start_policy_injector()
199
{
200
    storage = await get_storage();
201
    query_best = await get_query_best();
202

    
203
    let extra_opts = ["blocking", "responseHeaders"];
204
    if (is_chrome)
205
	extra_opts.push("extraHeaders");
206

    
207
    browser.webRequest.onBeforeRequest.addListener(
208
	url_inject,
209
	{
210
	    urls: ["<all_urls>"],
211
	    types: ["main_frame", "sub_frame"]
212
	},
213
	["blocking"]
214
    );
215

    
216
    browser.webRequest.onHeadersReceived.addListener(
217
	headers_inject,
218
	{
219
	    urls: ["<all_urls>"],
220
	    types: ["main_frame", "sub_frame"]
221
	},
222
	extra_opts
223
    );
224
}
225

    
226
/*
227
 * EXPORTS_START
228
 * EXPORT start_policy_injector
229
 * EXPORTS_END
230
 */
(4-4/7)