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 c22ce43f01
8 changed files with 45 additions and 15 deletions

View File

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

View File

@ -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:"});
}
}
}

View File

@ -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 <meta> CSP unless wrapped in
// - <html><head></head></head> 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);
}
}

View File

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

View File

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

View File

@ -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");

View File

@ -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) {

View File

@ -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",