/** * 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 gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url * IMPORT url_extract_target * IMPORT sign_policy * IMPORT get_query_best * IMPORT parse_csp * IMPORTS_END */ var storage; var query_best; const csp_header_names = { "content-security-policy" : true, "x-webkit-csp" : true, "x-content-security-policy" : true }; const unwanted_csp_directives = { "report-to" : true, "report-uri" : true, "script-src" : true, "script-src-elem" : true, "prefetch-src": true }; const header_name = "content-security-policy"; function not_csp_header(header) { return !csp_header_names[header.name.toLowerCase()]; } 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(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}; const headers = []; for (let header of details.responseHeaders) { if (not_csp_header(header)) { /* Retain all non-snitching headers */ if (header.name.toLowerCase() !== 'content-security-policy-report-only') headers.push(header); continue; } const csp = parse_csp(header.value); const rule = `'nonce-${targets.policy.nonce}'` /* TODO: confirm deleting non-existent things is OK everywhere */ /* No snitching or prefetching/rendering */ delete csp['report-to']; delete csp['report-uri']; if (!targets.policy.allow) { delete csp['script-src']; delete csp['script-src-elem']; csp['script-src-attr'] = ["'none'"]; csp['prefetch-src'] = ["'none'"]; } if ('script-src' in csp) csp['script-src'].push(rule); else csp['script-src'] = [rule]; if ('script-src-elem' in csp) csp['script-src-elem'].push(rule); else csp['script-src-elem'] = [rule]; /* TODO: is this safe */ let new_policy = Object.entries(csp).map( i => i[0] + ' ' + i[1].join(' ') + ';' ); headers.push({name: header.name, value: new_policy.join('')}); } return {responseHeaders: headers}; } async function start_policy_injector() { storage = await get_storage(); query_best = await get_query_best(); 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 */