aboutsummaryrefslogtreecommitdiff
path: root/background/policy_injector.js
blob: 702f879d08d995ec647e82f6c9bbec7024ed704b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
 * Hachette injecting policy to page using webRequest
 *
 * Copyright (C) 2021 Wojtek Kosior
 * Copyright (C) 2021 jahoti
 * Redistribution terms are gathered in the `copyright' file.
 */

/*
 * IMPORTS_START
 * IMPORT get_storage
 * IMPORT browser
 * IMPORT is_chrome
 * IMPORT gen_nonce
 * IMPORT is_privileged_url
 * IMPORT sign_data
 * IMPORT extract_signed
 * IMPORT query_best
 * IMPORT sanitize_csp_header
 * IMPORT csp_rule
 * IMPORTS_END
 */

var storage;

const csp_header_names = new Set([
    "content-security-policy",
    "x-webkit-csp",
    "x-content-security-policy"
]);

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

function headers_inject(details)
{
    const url = details.url;
    if (is_privileged_url(url))
	return;

    const [pattern, settings] = query_best(storage, url);
    const allow = !!(settings && settings.allow);
    const nonce = gen_nonce();
    const rule = `'nonce-${nonce}'`;

    let orig_csp_headers;
    let old_signature;
    let hachette_header;
    let headers = details.responseHeaders;

    for (const header of headers.filter(h => h.name === "x-hachette")) {
	const match = /^([^%])(%.*)$/.exec(header.value);
	if (!match)
	    continue;

	const old_data = extract_signed(...match.splice(1, 2), [[0]]);
	if (!old_data || old_data.url !== url)
	    continue;

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

	hachette_header = header;
	break;
    }

    if (!hachette_header) {
	hachette_header = {name: "x-hachette"};
	headers.push(hachette_header);
    }

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

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

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

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

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

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

    /* To ensure there is a CSP header if required */
    if (!allow)
	headers.push({name: "content-security-policy", value: csp_rule(nonce)});

    return {responseHeaders: headers};
}

async function start_policy_injector()
{
    storage = await get_storage();

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

    browser.webRequest.onHeadersReceived.addListener(
	headers_inject,
	{
	    urls: ["<all_urls>"],
	    types: ["main_frame", "sub_frame"]
	},
	extra_opts
    );
}

/*
 * EXPORTS_START
 * EXPORT start_policy_injector
 * EXPORTS_END
 */