From 46659614b97ead3b61f55c9ede7436ce8a612b35 Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 4 Dec 2020 19:38:40 +0100 Subject: [PATCH] More accurate blockage reporting, with better filtering of page's own CSP effects. --- src/bg/RequestGuard.js | 5 +++-- src/common/CapsCSP.js | 6 ++++-- src/common/RequestKey.js | 12 ++++++++++++ src/content/DocumentCSP.js | 8 ++++---- src/content/content.js | 27 +++++++++++++++++++++++---- src/content/onScriptDisabled.js | 5 ++++- src/content/staticNS.js | 2 +- src/lib/CSP.js | 5 ++++- src/manifest.json | 2 ++ 9 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/common/RequestKey.js diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index 8d1bfcb..b38d2e3 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -176,9 +176,10 @@ var RequestGuard = (() => { let tabId = sender.tab.id; let {frameId} = sender; let r = { - url, type, tabId, frameId + url, type, tabId, frameId, documentUrl: sender.url }; Content.reportTo(r, false, policyTypesMap[type]); + debug("Violation", type, url, tabId, frameId); if (type === "script" && url === sender.url) { TabStatus.record(r, "noscriptFrame", true); } else { @@ -539,7 +540,7 @@ var RequestGuard = (() => { if (!/:/.test(r.url)) r.url = request.documentUrl; Content.reportTo(r, false, policyTypesMap[r.type]); TabStatus.record(r, "blocked"); - } else if (report["violated-directive"] === "script-src" && (originalPolicy.includes("; script-src 'none'"))) { + } else if (report["violated-directive"].startsWith("script-src") && (originalPolicy.includes("script-src 'none'"))) { let r = fakeRequestFromCSP(report, request); Content.reportTo(r, false, "script"); // NEW TabStatus.record(r, "noscriptFrame", true); diff --git a/src/common/CapsCSP.js b/src/common/CapsCSP.js index cc1be72..4ac91f2 100644 --- a/src/common/CapsCSP.js +++ b/src/common/CapsCSP.js @@ -8,10 +8,12 @@ function CapsCSP(baseCSP = new CSP()) { let forbidData = new Set(this.dataUriTypes.filter(t => !capabilities.has(t))); let blockedTypes = new Set(this.types.filter(t => !capabilities.has(t))); if(!capabilities.has("script")) { + blockedTypes.add({name: "script-src-elem"}); + blockedTypes.add({name: "script-src-attr"}); blockedTypes.add("worker"); if (!blockedTypes.has("object")) { // data: URIs loaded in objects may run scripts - blockedTypes.add({name: "object", value: "http:"}); + blockedTypes.add({type: "object", value: "http:"}); } } @@ -20,7 +22,7 @@ function CapsCSP(baseCSP = new CSP()) { // for instance data: and blob: URIs for (let type of this.dataUriTypes) { if (blockedTypes.delete(type)) { - blockedTypes.add({name: type, value: "http:"}); + blockedTypes.add({type, value: "http:"}); } } } diff --git a/src/common/RequestKey.js b/src/common/RequestKey.js new file mode 100644 index 0000000..e5fcc9e --- /dev/null +++ b/src/common/RequestKey.js @@ -0,0 +1,12 @@ +'use strict'; + +var RequestKey = { + create(url, type, documentOrigin) { + return `${type}@${url}<${documentOrigin}`; + }, + + explode(requestKey) { + let [, type, url, documentOrigin] = /(\w+)@([^<]+)<(.*)/.exec(requestKey); + return {url, type, documentOrigin}; + } +}; diff --git a/src/content/DocumentCSP.js b/src/content/DocumentCSP.js index ee20548..a7b5251 100644 --- a/src/content/DocumentCSP.js +++ b/src/content/DocumentCSP.js @@ -19,7 +19,7 @@ class DocumentCSP { let csp = this.builder; let blocker = csp.buildFromCapabilities(capabilities, embedding); - if (!blocker) return true; + if (!blocker) return null; let createHTMLElement = tagName => document.createElementNS("http://www.w3.org/1999/xhtml", tagName); @@ -34,7 +34,7 @@ class DocumentCSP { try { if (!(document instanceof HTMLDocument)) { if (!(document instanceof XMLDocument)) { - return false; // nothing to do with ImageDocument, for instance + return null; // nothing to do with ImageDocument, for instance } // non-HTML XML documents ignore CSP unless wrapped in // - on Gecko @@ -62,8 +62,8 @@ class DocumentCSP { } } catch (e) { error(e, "Error inserting CSP %s in %s", document.URL, header && header.value); - return false; + return null; } - return true; + return CSP.normalize(header.value); } } diff --git a/src/content/content.js b/src/content/content.js index 7c49c1a..62a6ada 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -111,23 +111,42 @@ var notifyPage = async () => { window.addEventListener("pageshow", notifyPage); let violations = new Set(); +let documentOrigin = new URL(document.URL).origin; window.addEventListener("securitypolicyviolation", e => { if (!e.isTrusted) return; let {violatedDirective, originalPolicy} = e; - if (violatedDirective === `script-src 'none'`) onScriptDisabled(); + if (violatedDirective.startsWith(`script-src`) && originalPolicy.includes("script-src 'none'")) onScriptDisabled(); let type = violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script let url = e.blockedURI; if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) || ns.embeddingDocument || !document.querySelector("video,audio"))) return; - if (!(url && url.includes(":"))) { - url = document.URL; + if (!ns.CSP || !(CSP.normalize(originalPolicy).includes(ns.CSP))) { + // this seems to come from page's own CSP + return; } - let key = type + "@" + url; + let documentUrl = document.URL; + let origin; + if (!(url && url.includes(":"))) { + url = documentUrl; + origin = documentOrigin; + } else { + ({origin} = new URL(url)); + } + let key = RequestKey.create(origin, type, documentOrigin); if (violations.has(key)) return; violations.add(key); if (type === "frame") type = "sub_frame"; + seen.record({ + request: { + key, + url, + type, + documentUrl, + }, + allowed: false + }); Messages.send("violation", {url, type}); }, true); diff --git a/src/content/onScriptDisabled.js b/src/content/onScriptDisabled.js index 595906b..bbc4be3 100644 --- a/src/content/onScriptDisabled.js +++ b/src/content/onScriptDisabled.js @@ -1,6 +1,9 @@ function onScriptDisabled() { if (document.readyState === "loading") { - window.addEventListener("DOMContentLoaded", e => onScriptDisabled()); + if (!onScriptDisabled._installed) { + window.addEventListener("DOMContentLoaded", e => onScriptDisabled()); + onScriptDisabled._installed = true; + } return; } onScriptDisabled = () => {}; diff --git a/src/content/staticNS.js b/src/content/staticNS.js index 325ce7c..313666e 100644 --- a/src/content/staticNS.js +++ b/src/content/staticNS.js @@ -105,7 +105,7 @@ perms.capabilities.push("script"); } this.capabilities = new Set(perms.capabilities); - new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument); + this.CSP = new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument); } this.canScript = this.allows("script"); this.fire("capabilities"); diff --git a/src/lib/CSP.js b/src/lib/CSP.js index 69e134f..8549047 100644 --- a/src/lib/CSP.js +++ b/src/lib/CSP.js @@ -4,13 +4,16 @@ class CSP { static isMediaBlocker(csp) { return /(?:^|[\s;])media-src (?:'none'|http:)(?:;|$)/.test(csp); } + static normalize(csp) { + return csp.replace(/\s*;\s*/g, ';').replace(/\b(script-src\s+'none'.*?;)(?:script-src-\w+\s+'none';)+/, '$1'); + } build(...directives) { return directives.join(';'); } buildBlocker(...types) { - return this.build(...(types.map(type => `${type.name || type}-src ${type.value || "'none'"}`))); + return this.build(...(types.map(t => `${t.name || `${t.type || t}-src`} ${t.value || "'none'"}`))); } blocks(header, type) { diff --git a/src/manifest.json b/src/manifest.json index e66e07e..8e7b39d 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -48,6 +48,7 @@ "lib/NetCSP.js", "lib/TabCache.js", "common/CapsCSP.js", + "common/RequestKey.js", "common/Policy.js", "common/locale.js", "common/Entities.js", @@ -97,6 +98,7 @@ "lib/Messages.js", "lib/CSP.js", "common/CapsCSP.js", + "common/RequestKey.js", "content/DocumentCSP.js", "content/onScriptDisabled.js", "content/staticNS.js",