diff options
Diffstat (limited to 'src/bg/RequestGuard.js')
-rw-r--r-- | src/bg/RequestGuard.js | 218 |
1 files changed, 26 insertions, 192 deletions
diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index 7bdc929..5dea994 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -4,42 +4,7 @@ var RequestGuard = (() => { browser.browserAction.setTitle({title: VERSION_LABEL}); const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/"; const REPORT_GROUP = "NoScript-Endpoint"; - const REPORT_TO = { - name: "Report-To", - value: JSON.stringify({ "url": REPORT_URI, - "group": REPORT_GROUP, - "max-age": 10886400 }), - }; - const CSP = { - name: "content-security-policy", - start: `report-uri ${REPORT_URI};`, - end: `;report-to ${REPORT_GROUP};`, - isMine(header) { - let {name, value} = header; - if (name.toLowerCase() !== CSP.name) return false; - let startIdx = value.indexOf(this.start); - return startIdx > -1 && startIdx < value.lastIndexOf(this.end); - }, - inject(headerValue, mine) { - let startIdx = headerValue.indexOf(this.start); - if (startIdx < 0) return `${headerValue};${mine}`; - let endIdx = headerValue.lastIndexOf(this.end); - let retValue = `${headerValue.substring(0, startIdx)}${mine}`; - - return endIdx < 0 ? retValue : `${retValue}${headerValue.substring(endIdx + this.end.length + 1)}`; - }, - create(...directives) { - return `${this.start}${directives.join(';')}${this.end}`; - }, - createBlocker(...types) { - return this.create(...(types.map(type => `${type.name || type}-src ${type.value || "'none'"}`))); - }, - blocks(header, type) { - return header.includes(`;${type}-src 'none';`) - }, - types: ["script", "object", "media"], - }; - + let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP); const policyTypesMap = { main_frame: "", sub_frame: "frame", @@ -57,9 +22,6 @@ var RequestGuard = (() => { }; const allTypes = Object.keys(policyTypesMap); Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types - - const FORBID_DATAURI_TYPES = ["font", "media", "object"]; - const TabStatus = { map: new Map(), types: ["script", "object", "media", "frame", "font"], @@ -70,13 +32,11 @@ var RequestGuard = (() => { noscriptFrames: {}, } }, - initTab(tabId, records = this.newRecords()) { if (tabId < 0) return; this.map.set(tabId, records); return records; }, - _record(request, what, optValue) { let {tabId, frameId, type, url, documentUrl} = request; let policyType = policyTypesMap[type] || type; @@ -88,7 +48,6 @@ var RequestGuard = (() => { } else { records = this.initTab(tabId); } - if (what === "noscriptFrame" && type !== "object") { let nsf = records.noscriptFrames; nsf[frameId] = optValue; @@ -110,7 +69,6 @@ var RequestGuard = (() => { } return records; }, - record(request, what, optValue) { let {tabId} = request; if (tabId < 0) return; @@ -119,9 +77,7 @@ var RequestGuard = (() => { this.updateTab(request.tabId); } }, - _pendingTabs: new Set(), - updateTab(tabId) { if (tabId < 0) return; if (this._pendingTabs.size === 0) { @@ -139,22 +95,18 @@ var RequestGuard = (() => { let records = this.map.get(tabId) || this.initTab(tabId); let {allowed, blocked, noscriptFrames} = records; let topAllowed = !(noscriptFrames && noscriptFrames[0]); - let numAllowed = 0, numBlocked = 0, sum = 0; let report = this.types.map(t => { let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b; numAllowed+= a, numBlocked += b, sum += s; return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`; }).filter(s => s).join("\n"); - let enforced = ns.isEnforced(tabId); - let icon = topAllowed ? (numBlocked ? "part" : enforced ? "yes" : "global") : (numAllowed ? "sub" : "no"); let showBadge = ns.local.showCountBadge && numBlocked > 0; - let browserAction = browser.browserAction; browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}}); browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""}); @@ -165,11 +117,9 @@ var RequestGuard = (() => { : _("NotEnforced")}` }); }, - totalize(sum, value) { return sum + value; }, - async probe(tabId) { if (tabId === undefined) { (await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id)); @@ -181,7 +131,6 @@ var RequestGuard = (() => { } } }, - recordAll(tabId, seen) { if (seen) { let records = TabStatus.map.get(tabId); @@ -190,17 +139,21 @@ var RequestGuard = (() => { records.blocked = {}; } for (let thing of seen) { - thing.request.tabId = tabId; - TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked"); + let {request, allowed} = thing; + request.tabId = tabId; + debug(`Recording`, request); + TabStatus._record(request, allowed ? "allowed" : "blocked"); + if (request.key === "noscript-probe" && request.type === "main_frame" ) { + request.frameId = 0; + TabStatus._record(request, "noscriptFrame", !allowed); + } } this._updateTabNow(tabId); } }, - async onActivatedTab(info) { let {tabId} = info; let seen = await ns.collectSeen(tabId); - TabStatus.recordAll(tabId, seen); }, onRemovedTab(tabId) { @@ -209,12 +162,9 @@ var RequestGuard = (() => { } browser.tabs.onActivated.addListener(TabStatus.onActivatedTab); browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab); - if (!("setIcon" in browser.browserAction)) { // unsupported on Android TabStatus._updateTabNow = TabStatus.updateTab = () => {}; } - - let messageHandler = { async pageshow(message, sender) { TabStatus.recordAll(sender.tab.id, message.seen); @@ -249,7 +199,6 @@ var RequestGuard = (() => { if (!capabilities.has(policyType)) { perms = new Permissions(new Set(capabilities), false); perms.capabilities.add(policyType); - /* TODO: handle contextual permissions if (documentUrl) { let context = new URL(documentUrl).origin; @@ -262,23 +211,8 @@ var RequestGuard = (() => { } return true; }, - - async queryDocStatus(message, sender) { - let {frameId, tab} = sender; - let {url} = message; - let tabId = tab.id; - let records = TabStatus.map.get(tabId); - let noscriptFrames = records && records.noscriptFrames; - let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]); - let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script"); - debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript); - return {canScript, shouldScript}; - } - } - const Content = { - async reportTo(request, allowed, policyType) { let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request; let pending = pendingRequests.get(requestId); // null if from a CSP report @@ -310,7 +244,6 @@ var RequestGuard = (() => { } } }; - const pendingRequests = new Map(); function initPendingRequest(request) { let {requestId, url} = request; @@ -322,8 +255,6 @@ var RequestGuard = (() => { }); return redirected; } - - const ABORT = {cancel: true}, ALLOW = {}; const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/; const listeners = { @@ -341,7 +272,6 @@ var RequestGuard = (() => { // livemark request or similar browser-internal, always allow; return ALLOW; } - if (/^(?:data|blob):/.test(url)) { request._dataUrl = url; request.url = url = documentUrl; @@ -350,7 +280,6 @@ var RequestGuard = (() => { !ns.isEnforced(request.tabId) || policy.can(url, policyType, originUrl); Content.reportTo(request, allowed, policyType); - if (!allowed) { debug(`Blocking ${policyType}`, request); TabStatus.record(request, "blocked"); @@ -360,13 +289,10 @@ var RequestGuard = (() => { } catch (e) { error(e); } - return ALLOW; }, - async onHeadersReceived(request) { // called for main_frame, sub_frame and object - // check for duplicate calls let pending = pendingRequests.get(request.requestId); if (pending) { @@ -381,93 +307,28 @@ var RequestGuard = (() => { pending = pendingRequests.get(request.requestId); } pending.headersProcessed = true; - - let {url, documentUrl, statusCode, tabId, responseHeaders} = request; - + let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request; + let isMainFrame = type === "main_frame"; try { - let header, blocker; - let content = {}; - let hasReportTo = false; - for (let h of responseHeaders) { - if (CSP.isMine(h)) { - header = h; - h.value = CSP.inject(h.value, ""); - } else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) { - content[RegExp.$1.toLowerCase()] = h.value; - } else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) { - hasReportTo = true; - } - } - + let capabilities; if (ns.isEnforced(tabId)) { let policy = ns.policy; let perms = policy.get(url, documentUrl).perms; - if (policy.autoAllowTop && request.type === "main_frame" && perms === policy.DEFAULT) { + if (policy.autoAllowTop && isMainFrame && perms === policy.DEFAULT) { policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin); await ChildPolicies.update(policy); } - - let {capabilities} = perms; - let isObject = request.type === "object"; - if (isObject && !capabilities.has("webgl")) { // we can't inject webglHook - debug("Disabling scripts in object %s to prevent webgl abuse", url); - capabilities = new Set(capabilities); - capabilities.delete("script"); - let r = Object.assign({}, request, {type: "webgl"}); - TabStatus.record(r, "blocked"); - Content.reportTo(r, false, "webgl"); - } - let canScript = capabilities.has("script"); - - let blockedTypes; - let forbidData = new Set(FORBID_DATAURI_TYPES.filter(t => !capabilities.has(t))); - if (!content.disposition && - (!content.type || /^\s*(?:video|audio|application)\//.test(content.type))) { - debug(`Suspicious content type "%s" in request %o with capabilities %o`, - content.type, request, capabilities); - blockedTypes = new Set(CSP.types.filter(t => !capabilities.has(t))); - } else if(!canScript) { - blockedTypes = new Set(["script"]); - forbidData.add("object"); // data: URIs loaded in objects may run scripts - } else { - blockedTypes = new Set(); - } - - for (let type of forbidData) { // object, font, media - if (blockedTypes.has(type)) continue; - // HTTP is blocked in onBeforeRequest, let's allow it only and block - // for instance data: and blob: URIs - let dataBlocker = {name: type, value: "http: https:"}; - blockedTypes.add(dataBlocker) - } - - - if (blockedTypes.size) { - debug("Blocked types", blockedTypes); - blocker = CSP.createBlocker(...blockedTypes); - } - - if (request.type === "main_frame" && !TabStatus.map.has(tabId)) { - debug("No TabStatus data yet for noscriptFrame", tabId); - TabStatus.record(request, "noscriptFrame", true); - } + capabilities = perms.capabilities; } - - debug(`CSP blocker on %s:`, url, blocker); - if (blocker) { - if (header) { - header.value = CSP.inject(header.value, blocker); - } else { - header = {name: CSP.name, value: blocker}; - responseHeaders.push(header); - } + if (isMainFrame && !TabStatus.map.has(tabId)) { + debug("No TabStatus data yet for noscriptFrame", tabId); + TabStatus.record(request, "noscriptFrame", + capabilities && !capabilities.has("script")); } - + let header = csp.patchHeaders(responseHeaders, capabilities); if (header) { - if (blocker) pending.cspHeader = header; - if (!hasReportTo) { - responseHeaders.push(REPORT_TO); - } + pending.cspHeader = header; + debug(`CSP blocker on %s:`, url, header.value); return {responseHeaders}; } } catch (e) { @@ -475,7 +336,6 @@ var RequestGuard = (() => { } return ALLOW; }, - onResponseStarted(request) { debug("onResponseStarted", request); let {requestId, url, tabId, frameId, type} = request; @@ -483,32 +343,22 @@ var RequestGuard = (() => { TabStatus.initTab(tabId); } let scriptBlocked = request.responseHeaders.some( - h => CSP.isMine(h) && CSP.blocks(h.value, "script") + h => csp.isMine(h) && csp.blocks(h.value, "script") ); debug("%s scriptBlocked=%s setting noscriptFrame on ", url, scriptBlocked, tabId, frameId); TabStatus.record(request, "noscriptFrame", scriptBlocked); let pending = pendingRequests.get(requestId); if (pending) { pending.scriptBlocked = scriptBlocked; - if (!(pending.headersProcessed && + if (!(pending.headersProcessed && (scriptBlocked || !ns.isEnforced(tabId) || ns.policy.can(url, "script", request.documentURL)) )) { debug("[WARNING] onHeadersReceived %s %o", frameId, tabId, - pending.headersProcessed ? "has been overridden on": "could not process", + pending.headersProcessed ? "has been overridden on": "could not process", request); - - if (tabId !== -1 && type !== "object") { - debug("[WARNING] Reloading %s frame %s of tab %s.", url, frameId, tabId); - browser.tabs.executeScript(tabId, { - runAt: "document_start", - code: "window.location.reload(false)", - frameId - }); - } } } }, - onCompleted(request) { let {requestId} = request; if (pendingRequests.has(requestId)) { @@ -523,12 +373,10 @@ var RequestGuard = (() => { } } }, - onErrorOccurred(request) { pendingRequests.delete(request.requestId); } }; - function fakeRequestFromCSP(report, request) { let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script if (type === "frame") type = "sub_frame"; @@ -539,7 +387,6 @@ var RequestGuard = (() => { type, }); } - async function onViolationReport(request) { try { let decoder = new TextDecoder("UTF-8"); @@ -561,28 +408,21 @@ var RequestGuard = (() => { } return ABORT; } - const RequestGuard = { async start() { Messages.addHandler(messageHandler); - let wr = browser.webRequest; let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args); - let allUrls = ["<all_urls>"]; let docTypes = ["main_frame", "sub_frame", "object"]; - let filterDocs = {urls: allUrls, types: docTypes}; let filterAll = {urls: allUrls, types: allTypes}; - listen("onBeforeRequest", filterAll, ["blocking"]); - listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]); - (listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => { let {requestId, responseHeaders} = request; let pending = pendingRequests.get(request.requestId); - if (pending && pending.headersProcessed) { + if (pending && pending.headersProcessed) { let {cspHeader} = pending; if (cspHeader) { debug("Safety net: injecting again %o in %o", cspHeader, request); @@ -601,18 +441,13 @@ var RequestGuard = (() => { } return null; }, filterDocs, ["blocking", "responseHeaders"])).install(); - listen("onResponseStarted", filterDocs, ["responseHeaders"]); listen("onCompleted", filterAll); listen("onErrorOccurred", filterAll); - - wr.onBeforeRequest.addListener(onViolationReport, - {urls: [REPORT_URI], types: ["csp_report"]}, ["blocking", "requestBody"]); - + {urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]); TabStatus.probe(); }, - stop() { let wr = browser.webRequest; for (let [name, listener] of Object.entries(listeners)) { @@ -626,6 +461,5 @@ var RequestGuard = (() => { Messages.removeHandler(messageHandler); } }; - return RequestGuard; })(); |