More accurate blockage reporting, with better filtering of page's own CSP effects.
This commit is contained in:
parent
51dadae00a
commit
46659614b9
|
@ -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);
|
||||
|
|
|
@ -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:"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
function onScriptDisabled() {
|
||||
if (document.readyState === "loading") {
|
||||
if (!onScriptDisabled._installed) {
|
||||
window.addEventListener("DOMContentLoaded", e => onScriptDisabled());
|
||||
onScriptDisabled._installed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
onScriptDisabled = () => {};
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue