/** * Hachette injecting policy to page using webRequest * * Copyright (C) 2021 Wojtek Kosior * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ /* * IMPORTS_START * IMPORT TYPE_PREFIX * IMPORT get_storage * IMPORT browser * IMPORT is_chrome * IMPORT is_mozilla * IMPORT gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url * IMPORT url_item * IMPORT url_extract_target * IMPORT sign_policy * IMPORT query_best * IMPORT sanitize_csp_header * IMPORTS_END */ var storage; const csp_header_names = new Set([ "content-security-policy", "x-webkit-csp", "x-content-security-policy" ]); /* TODO: variable no longer in use; remove if not needed */ const unwanted_csp_directives = new Set([ "report-to", "report-uri", "script-src", "script-src-elem", "prefetch-src" ]); const report_only = "content-security-policy-report-only"; function url_inject(details) { if (is_privileged_url(details.url)) return; const targets = url_extract_target(details.url); if (targets.current) return; /* Redirect; update policy */ if (targets.policy) targets.target = ""; let [pattern, settings] = query_best(storage, targets.base_url); /* Defaults */ if (!pattern) settings = {}; const policy = encodeURIComponent( JSON.stringify({ allow: settings.allow, nonce: gen_nonce(), base_url: targets.base_url }) ); return { redirectUrl: [ targets.base_url, '#', sign_policy(policy, new Date()), policy, targets.target, targets.target2 ].join("") }; } function headers_inject(details) { const targets = url_extract_target(details.url); /* Block mis-/unsigned requests */ if (!targets.current) return {cancel: true}; let orig_csp_headers = is_chrome ? null : []; let headers = []; let csp_headers = is_chrome ? headers : []; const rule = `'nonce-${targets.policy.nonce}'`; const block = !targets.policy.allow; for (const header of details.responseHeaders) { if (!csp_header_names.has(header)) { /* Remove headers that only snitch on us */ if (header.name.toLowerCase() === report_only && block) continue; headers.push(header); /* If these are the original CSP headers, use them instead */ /* Test based on url_extract_target() in misc.js */ if (is_mozilla && header.name === "x-orig-csp") { let index = header.value.indexOf('%5B'); if (index === -1) continue; let sig = header.value.substring(0, index); let data = header.value.substring(index); if (sig !== sign_policy(data, 0)) continue; /* Confirmed- it's the originals, smuggled in! */ try { data = JSON.parse(decodeURIComponent(data)); } catch (e) { /* This should not be reached - it's our self-produced valid JSON. */ console.log("Unexpected internal error - invalid JSON smuggled!", e); } orig_csp_headers = csp_headers = null; for (const header of data) headers.push(sanitize_csp_header(header, rule, block)); } } else if (is_chrome || !orig_csp_headers) { csp_headers.push(sanitize_csp_header(header, rule, block)); if (is_mozilla) orig_csp_headers.push(header); } } if (orig_csp_headers) { /** Smuggle in the original CSP headers for future use. * These are signed with a time of 0, as it's not clear there * is a limit on how long Firefox might retain these headers in * the cache. */ orig_csp_headers = encodeURIComponent(JSON.stringify(orig_csp_headers)); headers.push({ name: "x-orig-csp", value: sign_policy(orig_csp_headers, 0) + orig_csp_headers }); headers = headers.concat(csp_headers); } /* To ensure there is a CSP header if required */ if (block) { headers.push({ name: "content-security-policy", value: `script-src ${rule}; script-src-elem ${rule}; ` + "script-src-attr 'none'; prefetch-src 'none';" }); } return {responseHeaders: headers}; } async function start_policy_injector() { storage = await get_storage(); let extra_opts = ["blocking", "responseHeaders"]; if (is_chrome) extra_opts.push("extraHeaders"); browser.webRequest.onBeforeRequest.addListener( url_inject, { urls: [""], types: ["main_frame", "sub_frame"] }, ["blocking"] ); browser.webRequest.onHeadersReceived.addListener( headers_inject, { urls: [""], types: ["main_frame", "sub_frame"] }, extra_opts ); } /* * EXPORTS_START * EXPORT start_policy_injector * EXPORTS_END */