Revision 97b8e30f
Added by jahoti about 2 years ago
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
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.