Project

General

Profile

« Previous | Next » 

Revision 97b8e30f

Added by jahoti about 2 years ago

Squash more CSP-filtering bugs

On Firefox, original CSP headers are now smuggled (signed) in an x-orig-csp
header to prevent re-processing issues with caching. Additionally, a default
header is added for non-whitelisted domains in case there are no existing
headers we can attach to.

View differences:

background/policy_injector.js
12 12
 * IMPORT get_storage
13 13
 * IMPORT browser
14 14
 * IMPORT is_chrome
15
 * IMPORT is_mozilla
15 16
 * IMPORT gen_unique
16 17
 * IMPORT gen_nonce
17 18
 * IMPORT is_privileged_url
......
39 40
    "prefetch-src": true
40 41
};
41 42

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

  
44 45
function not_csp_header(header)
45 46
{
......
82 83
    };
83 84
}
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

  
85 118
function headers_inject(details)
86 119
{
87 120
    const targets = url_extract_target(details.url);
......
89 122
    if (!targets.current)
90 123
	return {cancel: true};
91 124

  
92
    const headers = [];
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
    
93 132
    for (let header of details.responseHeaders) {
94 133
	if (not_csp_header(header)) {
95 134
	    /* Retain all non-snitching headers */
96
	    if (header.name.toLowerCase() !==
97
	        'content-security-policy-report-only')
135
	    if (header.name.toLowerCase() !== report_only) {
98 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
	    }
99 164

  
100 165
	    continue;
101 166
	}
102

  
103
	const csp = parse_csp(header.value);
104
	const rule = `'nonce-${targets.policy.nonce}'`
105
	
106
	/* TODO: confirm deleting non-existent things is OK everywhere */
107
	/* No snitching or prefetching/rendering */
108
	delete csp['report-to'];
109
	delete csp['report-uri'];
110
	
111
	if (!targets.policy.allow) {
112
	    delete csp['script-src'];
113
	    delete csp['script-src-elem'];
114
	    csp['script-src-attr'] = ["'none'"];
115
	    csp['prefetch-src'] = ["'none'"];
116
	}
117
	
118
	if ('script-src' in csp)
119
	    csp['script-src'].push(rule);
120
	else
121
	    csp['script-src'] = [rule];
122

  
123
	if ('script-src-elem' in csp)
124
	    csp['script-src-elem'].push(rule);
125
	else
126
	    csp['script-src-elem'] = [rule];
127
	
128
	/* TODO: is this safe */
129
	let new_policy = Object.entries(csp).map(
130
	    i => i[0] + ' ' + i[1].join(' ') + ';'
131
	);
167
	if (is_mozilla && !orig_csp_headers)
168
	    continue;
132 169
	
133
	headers.push({name: header.name, value: new_policy.join('')});
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
	});
134 197
    }
135 198

  
136 199
    return {responseHeaders: headers};

Also available in: Unified diff