Project

General

Profile

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

haketilo / background / policy_injector.js @ 24ad876c

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 = {
30
    "content-security-policy" : true,
31
    "x-webkit-csp" : true,
32
    "x-content-security-policy" : true
33
};
34

    
35
const unwanted_csp_directives = {
36
    "report-to" : true,
37
    "report-uri" : true,
38
    "script-src" : true,
39
    "script-src-elem" : true,
40
    "prefetch-src": true
41
};
42

    
43
const report_only = "content-security-policy-report-only";
44

    
45
function not_csp_header(header)
46
{
47
    return !csp_header_names[header.name.toLowerCase()];
48
}
49

    
50
function url_inject(details)
51
{
52
    if (is_privileged_url(details.url))
53
	return;
54

    
55
    const targets = url_extract_target(details.url);
56
    if (targets.current)
57
	return;
58

    
59
    /* Redirect; update policy */
60
    if (targets.policy)
61
	targets.target = "";
62

    
63
    let [pattern, settings] = query_best(targets.base_url);
64
    /* Defaults */
65
    if (!pattern)
66
	settings = {};
67

    
68
    const policy = encodeURIComponent(
69
	JSON.stringify({
70
	    allow: settings.allow,
71
	    nonce: gen_nonce(),
72
	    base_url: targets.base_url
73
	})
74
    );
75

    
76
    return {
77
	redirectUrl: [
78
	    targets.base_url,
79
	    '#', sign_policy(policy, new Date()), policy,
80
	    targets.target,
81
	    targets.target2
82
	].join("")
83
    };
84
}
85

    
86
function process_csp_header(header, rule, block)
87
{
88
    const csp = parse_csp(header.value);
89
    
90
    /* No snitching */
91
    delete csp['report-to'];
92
    delete csp['report-uri'];
93
    
94
    if (block) {
95
	delete csp['script-src'];
96
	delete csp['script-src-elem'];
97
	csp['script-src-attr'] = ["'none'"];
98
	csp['prefetch-src'] = ["'none'"];
99
    }
100
    
101
    if ('script-src' in csp)
102
	csp['script-src'].push(rule);
103
    else
104
	csp['script-src'] = [rule];
105

    
106
    if ('script-src-elem' in csp)
107
	csp['script-src-elem'].push(rule);
108
    else
109
	csp['script-src-elem'] = [rule];
110
    
111
    const new_policy = Object.entries(csp).map(
112
	i => i[0] + ' ' + i[1].join(' ') + ';'
113
    );
114
    
115
    return {name: header.name, value: new_policy.join('')}
116
}
117

    
118
function headers_inject(details)
119
{
120
    const targets = url_extract_target(details.url);
121
    /* Block mis-/unsigned requests */
122
    if (!targets.current)
123
	return {cancel: true};
124

    
125
    let orig_csp_headers = is_chrome ? null : [];
126
    let headers = [];
127
    let csp_headers = is_chrome ? headers : [];
128

    
129
    const rule = `'nonce-${targets.policy.nonce}'`;
130
    const block = !targets.policy.allow;
131
    
132
    for (let header of details.responseHeaders) {
133
	if (not_csp_header(header)) {
134
	    /* Retain all non-snitching headers */
135
	    if (header.name.toLowerCase() !== report_only) {
136
		headers.push(header);
137
		
138
		/* If these are the original CSP headers, use them instead */
139
		/* Test based on url_extract_target() in misc.js */
140
		if (is_mozilla && header.name === "x-orig-csp") {
141
		    let index = header.value.indexOf('%5B');
142
		    if (index === -1)
143
			continue;
144

    
145
		    let sig = header.value.substring(0, index);
146
		    let data = header.value.substring(index);
147
		    if (sig !== sign_policy(data, 0))
148
			continue;
149

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

    
159
		    orig_csp_headers = csp_headers = null;
160
		    for (let header of data)
161
			headers.push(process_csp_header(header, rule, block));
162
		}
163
	    }
164

    
165
	    continue;
166
	}
167
	if (is_mozilla && !orig_csp_headers)
168
	    continue;
169
	
170
	csp_headers.push(process_csp_header(header, rule, block));
171
	if (is_mozilla)
172
	    orig_csp_headers.push(header);
173
    }
174

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

    
187
	headers = headers.concat(csp_headers);
188
    }
189

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

    
199
    return {responseHeaders: headers};
200
}
201

    
202
async function start_policy_injector()
203
{
204
    storage = await get_storage();
205
    query_best = await get_query_best();
206

    
207
    let extra_opts = ["blocking", "responseHeaders"];
208
    if (is_chrome)
209
	extra_opts.push("extraHeaders");
210

    
211
    browser.webRequest.onBeforeRequest.addListener(
212
	url_inject,
213
	{
214
	    urls: ["<all_urls>"],
215
	    types: ["main_frame", "sub_frame"]
216
	},
217
	["blocking"]
218
    );
219

    
220
    browser.webRequest.onHeadersReceived.addListener(
221
	headers_inject,
222
	{
223
	    urls: ["<all_urls>"],
224
	    types: ["main_frame", "sub_frame"]
225
	},
226
	extra_opts
227
    );
228
}
229

    
230
/*
231
 * EXPORTS_START
232
 * EXPORT start_policy_injector
233
 * EXPORTS_END
234
 */
(4-4/7)