More accurate blockage reporting, with better filtering of page's own CSP effects.

This commit is contained in:
hackademix 2020-12-04 19:38:40 +01:00
parent 51dadae00a
commit 46659614b9
9 changed files with 57 additions and 15 deletions

View File

@ -176,9 +176,10 @@ var RequestGuard = (() => {
let tabId = sender.tab.id; let tabId = sender.tab.id;
let {frameId} = sender; let {frameId} = sender;
let r = { let r = {
url, type, tabId, frameId url, type, tabId, frameId, documentUrl: sender.url
}; };
Content.reportTo(r, false, policyTypesMap[type]); Content.reportTo(r, false, policyTypesMap[type]);
debug("Violation", type, url, tabId, frameId);
if (type === "script" && url === sender.url) { if (type === "script" && url === sender.url) {
TabStatus.record(r, "noscriptFrame", true); TabStatus.record(r, "noscriptFrame", true);
} else { } else {
@ -539,7 +540,7 @@ var RequestGuard = (() => {
if (!/:/.test(r.url)) r.url = request.documentUrl; if (!/:/.test(r.url)) r.url = request.documentUrl;
Content.reportTo(r, false, policyTypesMap[r.type]); Content.reportTo(r, false, policyTypesMap[r.type]);
TabStatus.record(r, "blocked"); 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); let r = fakeRequestFromCSP(report, request);
Content.reportTo(r, false, "script"); // NEW Content.reportTo(r, false, "script"); // NEW
TabStatus.record(r, "noscriptFrame", true); TabStatus.record(r, "noscriptFrame", true);

View File

@ -8,10 +8,12 @@ function CapsCSP(baseCSP = new CSP()) {
let forbidData = new Set(this.dataUriTypes.filter(t => !capabilities.has(t))); let forbidData = new Set(this.dataUriTypes.filter(t => !capabilities.has(t)));
let blockedTypes = new Set(this.types.filter(t => !capabilities.has(t))); let blockedTypes = new Set(this.types.filter(t => !capabilities.has(t)));
if(!capabilities.has("script")) { if(!capabilities.has("script")) {
blockedTypes.add({name: "script-src-elem"});
blockedTypes.add({name: "script-src-attr"});
blockedTypes.add("worker"); blockedTypes.add("worker");
if (!blockedTypes.has("object")) { if (!blockedTypes.has("object")) {
// data: URIs loaded in objects may run scripts // 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 instance data: and blob: URIs
for (let type of this.dataUriTypes) { for (let type of this.dataUriTypes) {
if (blockedTypes.delete(type)) { if (blockedTypes.delete(type)) {
blockedTypes.add({name: type, value: "http:"}); blockedTypes.add({type, value: "http:"});
} }
} }
} }

12
src/common/RequestKey.js Normal file
View File

@ -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};
}
};

View File

@ -19,7 +19,7 @@ class DocumentCSP {
let csp = this.builder; let csp = this.builder;
let blocker = csp.buildFromCapabilities(capabilities, embedding); let blocker = csp.buildFromCapabilities(capabilities, embedding);
if (!blocker) return true; if (!blocker) return null;
let createHTMLElement = let createHTMLElement =
tagName => document.createElementNS("http://www.w3.org/1999/xhtml", tagName); tagName => document.createElementNS("http://www.w3.org/1999/xhtml", tagName);
@ -34,7 +34,7 @@ class DocumentCSP {
try { try {
if (!(document instanceof HTMLDocument)) { if (!(document instanceof HTMLDocument)) {
if (!(document instanceof XMLDocument)) { 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 <meta> CSP unless wrapped in // non-HTML XML documents ignore <meta> CSP unless wrapped in
// - <html><head></head></head> on Gecko // - <html><head></head></head> on Gecko
@ -62,8 +62,8 @@ class DocumentCSP {
} }
} catch (e) { } catch (e) {
error(e, "Error inserting CSP %s in %s", document.URL, header && header.value); error(e, "Error inserting CSP %s in %s", document.URL, header && header.value);
return false; return null;
} }
return true; return CSP.normalize(header.value);
} }
} }

View File

@ -111,23 +111,42 @@ var notifyPage = async () => {
window.addEventListener("pageshow", notifyPage); window.addEventListener("pageshow", notifyPage);
let violations = new Set(); let violations = new Set();
let documentOrigin = new URL(document.URL).origin;
window.addEventListener("securitypolicyviolation", e => { window.addEventListener("securitypolicyviolation", e => {
if (!e.isTrusted) return; if (!e.isTrusted) return;
let {violatedDirective, originalPolicy} = e; 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 type = violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script
let url = e.blockedURI; let url = e.blockedURI;
if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) || if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) ||
ns.embeddingDocument || !document.querySelector("video,audio"))) ns.embeddingDocument || !document.querySelector("video,audio")))
return; return;
if (!(url && url.includes(":"))) { if (!ns.CSP || !(CSP.normalize(originalPolicy).includes(ns.CSP))) {
url = document.URL; // 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; if (violations.has(key)) return;
violations.add(key); violations.add(key);
if (type === "frame") type = "sub_frame"; if (type === "frame") type = "sub_frame";
seen.record({
request: {
key,
url,
type,
documentUrl,
},
allowed: false
});
Messages.send("violation", {url, type}); Messages.send("violation", {url, type});
}, true); }, true);

View File

@ -1,6 +1,9 @@
function onScriptDisabled() { function onScriptDisabled() {
if (document.readyState === "loading") { if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", e => onScriptDisabled()); if (!onScriptDisabled._installed) {
window.addEventListener("DOMContentLoaded", e => onScriptDisabled());
onScriptDisabled._installed = true;
}
return; return;
} }
onScriptDisabled = () => {}; onScriptDisabled = () => {};

View File

@ -105,7 +105,7 @@
perms.capabilities.push("script"); perms.capabilities.push("script");
} }
this.capabilities = new Set(perms.capabilities); 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.canScript = this.allows("script");
this.fire("capabilities"); this.fire("capabilities");

View File

@ -4,13 +4,16 @@ class CSP {
static isMediaBlocker(csp) { static isMediaBlocker(csp) {
return /(?:^|[\s;])media-src (?:'none'|http:)(?:;|$)/.test(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) { build(...directives) {
return directives.join(';'); return directives.join(';');
} }
buildBlocker(...types) { 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) { blocks(header, type) {

View File

@ -48,6 +48,7 @@
"lib/NetCSP.js", "lib/NetCSP.js",
"lib/TabCache.js", "lib/TabCache.js",
"common/CapsCSP.js", "common/CapsCSP.js",
"common/RequestKey.js",
"common/Policy.js", "common/Policy.js",
"common/locale.js", "common/locale.js",
"common/Entities.js", "common/Entities.js",
@ -97,6 +98,7 @@
"lib/Messages.js", "lib/Messages.js",
"lib/CSP.js", "lib/CSP.js",
"common/CapsCSP.js", "common/CapsCSP.js",
"common/RequestKey.js",
"content/DocumentCSP.js", "content/DocumentCSP.js",
"content/onScriptDisabled.js", "content/onScriptDisabled.js",
"content/staticNS.js", "content/staticNS.js",