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.