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.