Revision 44e89d8e
Added by koszko almost 2 years ago
background/policy_injector.js | ||
---|---|---|
10 | 10 |
* IMPORTS_START |
11 | 11 |
* IMPORT sign_data |
12 | 12 |
* IMPORT extract_signed |
13 |
* IMPORT sanitize_csp_header |
|
14 |
* IMPORT csp_rule |
|
15 |
* IMPORT is_csp_header_name |
|
13 |
* IMPORT make_csp_rule |
|
14 |
* IMPORT csp_header_regex |
|
16 | 15 |
* IMPORTS_END |
17 | 16 |
*/ |
18 | 17 | |
... | ... | |
43 | 42 |
break; |
44 | 43 |
} |
45 | 44 | |
45 |
if (policy.has_payload) { |
|
46 |
csp_headers = []; |
|
47 |
const non_csp_headers = []; |
|
48 |
const header_list = |
|
49 |
h => csp_header_regex.test(h) ? csp_headers : non_csp_headers; |
|
50 |
headers.forEach(h => header_list(h.name).push(h)); |
|
51 |
headers = non_csp_headers; |
|
52 |
} else { |
|
53 |
headers.push(...csp_headers || []); |
|
54 |
} |
|
55 | ||
46 | 56 |
if (!hachette_header) { |
47 | 57 |
hachette_header = {name: "x-hachette"}; |
48 | 58 |
headers.push(hachette_header); |
49 | 59 |
} |
50 | 60 | |
51 |
csp_headers = csp_headers || |
|
52 |
headers.filter(h => is_csp_header_name(h.name)); |
|
53 | ||
54 |
/* When blocking remove report-only CSP headers that snitch on us. */ |
|
55 |
headers = headers.filter(h => !is_csp_header_name(h.name, !policy.allow)); |
|
56 | ||
57 | 61 |
if (old_signature) |
58 | 62 |
headers = headers.filter(h => h.value.search(old_signature) === -1); |
59 | 63 | |
60 |
headers.push(...csp_headers.map(h => sanitize_csp_header(h, policy))); |
|
61 | ||
62 | 64 |
const policy_str = encodeURIComponent(JSON.stringify(policy)); |
63 | 65 |
const signed_policy = sign_data(policy_str, new Date().getTime()); |
64 | 66 |
const later_30sec = new Date(new Date().getTime() + 30000).toGMTString(); |
... | ... | |
76 | 78 |
hachette_data = encodeURIComponent(JSON.stringify(hachette_data)); |
77 | 79 |
hachette_header.value = sign_data(hachette_data, 0).join("_"); |
78 | 80 | |
79 |
/* To ensure there is a CSP header if required */ |
|
80 |
if (!policy.allow) |
|
81 |
if (!policy.allow) { |
|
81 | 82 |
headers.push({ |
82 | 83 |
name: "content-security-policy", |
83 |
value: csp_rule(policy.nonce)
|
|
84 |
value: make_csp_rule(policy)
|
|
84 | 85 |
}); |
86 |
} |
|
85 | 87 | |
86 | 88 |
return headers; |
87 | 89 |
} |
background/stream_filter.js | ||
---|---|---|
12 | 12 |
/* |
13 | 13 |
* IMPORTS_START |
14 | 14 |
* IMPORT browser |
15 |
* IMPORT is_csp_header_name
|
|
15 |
* IMPORT csp_header_regex
|
|
16 | 16 |
* IMPORTS_END |
17 | 17 |
*/ |
18 | 18 | |
... | ... | |
116 | 116 |
const doc = new DOMParser().parseFromString(html, "text/html"); |
117 | 117 | |
118 | 118 |
for (const meta of doc.querySelectorAll("head>meta[http-equiv]")) { |
119 |
if (is_csp_header_name(meta.getAttribute("http-equiv"), true) && |
|
120 |
meta.content) |
|
119 |
if (csp_header_regex.test(meta.httpEquiv) && meta.content) |
|
121 | 120 |
return true; |
122 | 121 |
} |
123 | 122 |
common/misc.js | ||
---|---|---|
43 | 43 |
return Uint8toHex(randomData); |
44 | 44 |
} |
45 | 45 | |
46 |
/* csp rule that blocks all scripts except for those injected by us */
|
|
47 |
function csp_rule(nonce)
|
|
46 |
/* CSP rule that blocks scripts according to policy's needs. */
|
|
47 |
function make_csp_rule(policy)
|
|
48 | 48 |
{ |
49 |
const rule = `'nonce-${nonce}'`; |
|
50 |
return `script-src ${rule}; script-src-elem ${rule}; script-src-attr 'none'; prefetch-src 'none';`; |
|
49 |
let rule = "prefetch-src 'none'; script-src-attr 'none';"; |
|
50 |
const script_src = policy.has_payload ? |
|
51 |
`'nonce-${policy.nonce}'` : "'none'"; |
|
52 |
rule += ` script-src ${script_src}; script-src-elem ${script_src};`; |
|
53 |
return rule; |
|
51 | 54 |
} |
52 | 55 | |
53 | 56 |
/* Check if some HTTP header might define CSP rules. */ |
54 |
const csp_header_names = new Set([ |
|
55 |
"content-security-policy", |
|
56 |
"x-webkit-csp", |
|
57 |
"x-content-security-policy" |
|
58 |
]); |
|
59 | ||
60 |
const report_only_header_name = "content-security-policy-report-only"; |
|
61 | ||
62 |
function is_csp_header_name(string, include_report_only) |
|
63 |
{ |
|
64 |
string = string && string.toLowerCase().trim() || ""; |
|
65 | ||
66 |
return (include_report_only && string === report_only_header_name) || |
|
67 |
csp_header_names.has(string); |
|
68 |
} |
|
57 |
const csp_header_regex = |
|
58 |
/^\s*(content-security-policy|x-webkit-csp|x-content-security-policy)/i; |
|
69 | 59 | |
70 | 60 |
/* |
71 | 61 |
* Print item together with type, e.g. |
... | ... | |
111 | 101 |
return directives; |
112 | 102 |
} |
113 | 103 | |
114 |
/* Make CSP headers do our bidding, not interfere */ |
|
115 |
function sanitize_csp_header(header, policy) |
|
116 |
{ |
|
117 |
const rule = `'nonce-${policy.nonce}'`; |
|
118 |
const csp = parse_csp(header.value); |
|
119 | ||
120 |
if (!policy.allow) { |
|
121 |
/* No snitching */ |
|
122 |
delete csp['report-to']; |
|
123 |
delete csp['report-uri']; |
|
124 | ||
125 |
delete csp['script-src']; |
|
126 |
delete csp['script-src-elem']; |
|
127 | ||
128 |
csp['script-src-attr'] = ["'none'"]; |
|
129 |
csp['prefetch-src'] = ["'none'"]; |
|
130 |
} |
|
131 | ||
132 |
if ('script-src' in csp) |
|
133 |
csp['script-src'].push(rule); |
|
134 |
else |
|
135 |
csp['script-src'] = [rule]; |
|
136 | ||
137 |
if ('script-src-elem' in csp) |
|
138 |
csp['script-src-elem'].push(rule); |
|
139 |
else |
|
140 |
csp['script-src-elem'] = [rule]; |
|
141 | ||
142 |
const new_csp = Object.entries(csp).map( |
|
143 |
i => `${i[0]} ${i[1].join(' ')};` |
|
144 |
); |
|
145 | ||
146 |
return {name: header.name, value: new_csp.join('')}; |
|
147 |
} |
|
148 | ||
149 | 104 |
/* Regexes and objects to use as/in schemas for parse_json_with_schema(). */ |
150 | 105 |
const nonempty_string_matcher = /.+/; |
151 | 106 | |
... | ... | |
161 | 116 |
/* |
162 | 117 |
* EXPORTS_START |
163 | 118 |
* EXPORT gen_nonce |
164 |
* EXPORT csp_rule |
|
165 |
* EXPORT is_csp_header_name
|
|
119 |
* EXPORT make_csp_rule
|
|
120 |
* EXPORT csp_header_regex
|
|
166 | 121 |
* EXPORT nice_name |
167 | 122 |
* EXPORT open_in_settings |
168 | 123 |
* EXPORT is_privileged_url |
169 |
* EXPORT sanitize_csp_header |
|
170 | 124 |
* EXPORT matchers |
171 | 125 |
* EXPORTS_END |
172 | 126 |
*/ |
content/main.js | ||
---|---|---|
16 | 16 |
* IMPORT is_chrome |
17 | 17 |
* IMPORT is_mozilla |
18 | 18 |
* IMPORT start_activity_info_server |
19 |
* IMPORT csp_rule |
|
20 |
* IMPORT is_csp_header_name |
|
21 |
* IMPORT sanitize_csp_header |
|
19 |
* IMPORT make_csp_rule |
|
20 |
* IMPORT csp_header_regex |
|
22 | 21 |
* IMPORTS_END |
23 | 22 |
*/ |
24 | 23 | |
... | ... | |
172 | 171 |
const name = construct_name.join(""); |
173 | 172 |
seta(node, `${blocked_str}-${name}`, geta(node, name)); |
174 | 173 |
} |
175 |
} |
|
176 | ||
177 |
function sanitize_meta(meta, policy) |
|
178 |
{ |
|
179 |
const value = meta.content || ""; |
|
180 | 174 | |
181 |
if (!value || !is_csp_header_name(meta.httpEquiv || "", true)) |
|
182 |
return; |
|
183 | ||
184 |
block_attribute(meta, "content"); |
|
175 |
rema(node, attr); |
|
185 | 176 |
} |
186 | 177 | |
187 | 178 |
/* |
188 |
* Used to disable <script> that has not yet been added to live DOM (doesn't
|
|
189 |
* work for those already added). |
|
179 |
* Used to disable `<script>'s and `<meta>'s that have not yet been added to
|
|
180 |
* live DOM (doesn't work for those already added).
|
|
190 | 181 |
*/ |
182 |
function sanitize_meta(meta) |
|
183 |
{ |
|
184 |
if (csp_header_regex.test(meta.httpEquiv) && meta.content) |
|
185 |
block_attribute(meta, "content"); |
|
186 |
} |
|
187 | ||
191 | 188 |
function sanitize_script(script) |
192 | 189 |
{ |
193 | 190 |
script.hachette_blocked_type = script.getAttribute("type"); |
... | ... | |
195 | 192 |
} |
196 | 193 | |
197 | 194 |
/* |
198 |
* Executed after script has been connected to the DOM, when it is no longer
|
|
199 |
* eligible for being executed by the browser |
|
195 |
* Executed after `<script>' has been connected to the DOM, when it is no longer
|
|
196 |
* eligible for being executed by the browser.
|
|
200 | 197 |
*/ |
201 |
function desanitize_script(script, policy)
|
|
198 |
function desanitize_script(script) |
|
202 | 199 |
{ |
203 | 200 |
script.setAttribute("type", script.hachette_blocked_type); |
204 | 201 | |
205 |
if (script.hachette_blocked_type === null)
|
|
202 |
if ([null, undefined].includes(script.hachette_blocked_type))
|
|
206 | 203 |
script.removeAttribute("type"); |
207 | 204 | |
208 | 205 |
delete script.hachette_blocked_type; |
... | ... | |
233 | 230 |
* cause part of the DOM to be loaded when our content scripts get to run. Thus, |
234 | 231 |
* before the CSP rules we inject (for non-HTTP pages) become effective, we need |
235 | 232 |
* to somehow block the execution of `<script>'s and intrinsics that were |
236 |
* already there. |
|
233 |
* already there. Additionally, some browsers (IceCat 60) seem to have problems |
|
234 |
* applying this CSP to non-inline `<scripts>' in certain scenarios. |
|
237 | 235 |
*/ |
236 |
function prevent_script_execution(event) |
|
237 |
{ |
|
238 |
if (!event.target._hachette_payload) |
|
239 |
event.preventDefault(); |
|
240 |
} |
|
241 | ||
238 | 242 |
function mozilla_initial_block(doc) |
239 | 243 |
{ |
240 |
const blocker = e => e.preventDefault(); |
|
241 |
doc.addEventListener("beforescriptexecute", blocker); |
|
242 |
setTimeout(() => doc.removeEventListener("beforescriptexecute", blocker)); |
|
244 |
doc.addEventListener("beforescriptexecute", prevent_script_execution); |
|
243 | 245 | |
244 | 246 |
[...doc.all].flatMap(ele => [...ele.attributes].map(attr => [ele, attr])) |
245 | 247 |
.map(([ele, attr]) => [ele, attr.localName]) |
... | ... | |
273 | 275 |
* non-HTML documents. |
274 | 276 |
*/ |
275 | 277 |
const html = new DOMParser().parseFromString(`<html><head><meta \ |
276 |
http-equiv="Content-Security-Policy" content="${csp_rule(policy.nonce)}"\
|
|
278 |
http-equiv="Content-Security-Policy" content="${make_csp_rule(policy)}"\
|
|
277 | 279 |
/></head><body>Loading...</body></html>`, "text/html").documentElement; |
278 | 280 | |
279 | 281 |
/* |
... | ... | |
284 | 286 |
root.replaceWith(html); |
285 | 287 | |
286 | 288 |
/* |
287 |
* For XML documents, we don't intend to inject payload, so we neither block
|
|
288 |
* document's CSP `<meta>' tags nor wait for `<head>' to be parsed.
|
|
289 |
* When we don't inject payload, we neither block document's CSP `<meta>'
|
|
290 |
* tags nor wait for `<head>' to be parsed. |
|
289 | 291 |
*/ |
290 |
if (document instanceof HTMLDocument) {
|
|
292 |
if (policy.has_payload) {
|
|
291 | 293 |
await wait_for_head(doc, root); |
292 | 294 | |
293 | 295 |
root.querySelectorAll("head meta") |
... | ... | |
333 | 335 |
policy = {allow: false, nonce: gen_nonce()}; |
334 | 336 |
} |
335 | 337 | |
338 |
if (!(document instanceof HTMLDocument)) |
|
339 |
policy.has_payload = false; |
|
340 | ||
336 | 341 |
console.debug("current policy", policy); |
337 | 342 | |
338 | 343 |
const doc_ready = Promise.all([ |
Also available in: Unified diff
simplify CSP handling
All page's CSP rules are now removed when a payload is to be injected. When there is no payload, CSP rules are not modified but only supplemented with Hachette's own.