Switch from HTTP to DOM event based CSP reporting in compatible browsers.

This commit is contained in:
hackademix 2020-07-09 22:01:44 +02:00
parent b2ac66f36f
commit e7b6805bb4
3 changed files with 49 additions and 35 deletions

View File

@ -1,31 +1,18 @@
"use strict";
function ReportingCSP(reportURI, reportGroup) {
const REPORT_TO_SUPPORTED = false;
// TODO: figure out if we're running on a browser supporting the report-to
// CSP directive, breaking report-uri, see
// 1. https://www.w3.org/TR/CSP3/#directive-report-uri
// 2. https://bugs.chromium.org/p/chromium/issues/detail?id=726634
// 3. https://bugzilla.mozilla.org/show_bug.cgi?id=1391243
const REPORT_TO = {
name: "Report-To",
value: JSON.stringify({ "url": reportURI,
"group": reportGroup,
"max-age": 10886400 }),
};
function ReportingCSP(marker, reportURI = "") {
const DOM_SUPPORTED = "SecurityPolicyViolationEvent" in window;
if (DOM_SUPPORTED) reportURI = "";
return Object.assign(
new CapsCSP(new NetCSP(
REPORT_TO_SUPPORTED ? `;report-to ${reportGroup};`
: `report-uri ${reportURI};`
new CapsCSP(new NetCSP(
reportURI ? `report-uri ${reportURI};` : marker
)),
{
reportURI,
reportGroup,
patchHeaders(responseHeaders, capabilities) {
let header = null;
let needsReportTo = REPORT_TO_SUPPORTED;
let blocker = capabilities && this.buildFromCapabilities(capabilities);
let extras = [];
responseHeaders.forEach((h, index) => {
@ -40,9 +27,6 @@ function ReportingCSP(reportURI, reportGroup) {
extras.push(...this.unmergeExtras(h));
}
responseHeaders.splice(index, 1);
} else if (needsReportTo &&
h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
needsReportTo = false;
} else if (blocker && /^(Location|Refresh)$/i.test(h.name)) {
// neutralize any HTTP redirection to data: URLs, like Chromium
let url = /^R/i.test(h.name)
@ -54,9 +38,6 @@ function ReportingCSP(reportURI, reportGroup) {
});
if (blocker) {
if (needsReportTo) {
responseHeaders.push(REPORT_TO);
}
header = this.asHeader(blocker);
responseHeaders.push(header);
}

View File

@ -2,9 +2,9 @@ var RequestGuard = (() => {
'use strict';
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
browser.browserAction.setTitle({title: VERSION_LABEL});
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
const REPORT_GROUP = "NoScript-Endpoint";
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
const CSP_REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
const CSP_MARKER = "noscript-marker";
let csp = new ReportingCSP(CSP_MARKER, CSP_REPORT_URI);
const policyTypesMap = {
main_frame: "",
sub_frame: "frame",
@ -172,6 +172,19 @@ var RequestGuard = (() => {
TabStatus.recordAll(sender.tab.id, message.seen);
return true;
},
violation({url, type}, sender) {
let tabId = sender.tab.id;
let {frameId} = sender;
let r = {
url, type, tabId, frameId
};
Content.reportTo(r, false, policyTypesMap[type]);
if (type === "script" && url === sender.url) {
TabStatus.record(r, "noscriptFrame", true);
} else {
TabStatus.record(r, "blocked");
}
},
async blockedObjects(message, sender) {
let {url, documentUrl, policyType} = message;
let TAG = `<${policyType.toUpperCase()}>`;
@ -501,11 +514,14 @@ var RequestGuard = (() => {
type,
});
}
let utf8Decoder = new TextDecoder("UTF-8");
function onViolationReport(request) {
try {
let decoder = new TextDecoder("UTF-8");
const report = JSON.parse(decoder.decode(request.requestBody.raw[0].bytes))['csp-report'];
let csp = report["original-policy"]
let text = utf8Decoder.decode(request.requestBody.raw[0].bytes);
if (text.includes(`"inline"`)) return ABORT;
let report = JSON.parse(text)["csp-report"];
let originalPolicy = report["original-policy"]
debug("CSP report", report);
let blockedURI = report['blocked-uri'];
if (blockedURI && blockedURI !== 'self') {
@ -513,7 +529,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" && /; script-src 'none'/.test(report["original-policy"])) {
} else if (report["violated-directive"] === "script-src" && (originalPolicy.includes("; script-src 'none'"))) {
let r = fakeRequestFromCSP(report, request);
Content.reportTo(r, false, "script"); // NEW
TabStatus.record(r, "noscriptFrame", true);
@ -570,8 +586,10 @@ var RequestGuard = (() => {
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll);
wr.onBeforeRequest.addListener(onViolationReport,
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
if (csp.reportURI) {
wr.onBeforeRequest.addListener(onViolationReport,
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
}
TabStatus.probe();
},
stop() {

View File

@ -84,6 +84,21 @@ var notifyPage = async () => {
window.addEventListener("pageshow", notifyPage);
let violations = new Set();
window.addEventListener("securitypolicyviolation", e => {
if (!e.isTrusted) return;
let type = e.violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script
let url = e.blockedURI;
if (!(url && url.includes(":"))) {
url = document.URL;
}
let key = type + "@" + url;
if (violations.has(key)) return;
violations.add(key);
if (type === "frame") type = "sub_frame";
Messages.send("violation", {url, type});
}, true);
ns.on("capabilities", () => {
seen.record({
request: {