Refactoring CSP building out of RequestGuard.

This commit is contained in:
hackademix 2018-08-26 16:33:40 +02:00
parent b5d7266c50
commit e82e961dd7
6 changed files with 143 additions and 97 deletions

20
src/bg/ReportingCSP.js Normal file
View File

@ -0,0 +1,20 @@
"use strict";
function ReportingCSP(reportURI, reportGroup) {
return Object.assign(
new CapsCSP(new NetCSP(
`report-uri ${reportURI};`,
`;report-to ${reportGroup};`
)),
{
reportURI,
reportGroup,
reportToHeader: {
name: "Report-To",
value: JSON.stringify({ "url": reportURI,
"group": reportGroup,
"max-age": 10886400 }),
}
}
);
}

View File

@ -2,44 +2,12 @@ var RequestGuard = (() => {
'use strict'; 'use strict';
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`; const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
browser.browserAction.setTitle({title: VERSION_LABEL}); browser.browserAction.setTitle({title: VERSION_LABEL});
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/"; const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
const REPORT_GROUP = "NoScript-Endpoint"; const REPORT_GROUP = "NoScript-Endpoint";
const REPORT_TO = {
name: "Report-To", let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
value: JSON.stringify({ "url": REPORT_URI,
"group": REPORT_GROUP,
"max-age": 10886400 }),
};
const CSP = {
name: "content-security-policy",
start: `report-uri ${REPORT_URI};`,
end: `;report-to ${REPORT_GROUP};`,
isMine(header) {
let {name, value} = header;
if (name.toLowerCase() !== CSP.name) return false;
let startIdx = value.indexOf(this.start);
return startIdx > -1 && startIdx < value.lastIndexOf(this.end);
},
inject(headerValue, mine) {
let startIdx = headerValue.indexOf(this.start);
if (startIdx < 0) return `${headerValue};${mine}`;
let endIdx = headerValue.lastIndexOf(this.end);
let retValue = `${headerValue.substring(0, startIdx)}${mine}`;
return endIdx < 0 ? retValue : `${retValue}${headerValue.substring(endIdx + this.end.length + 1)}`;
},
create(...directives) {
return `${this.start}${directives.join(';')}${this.end}`;
},
createBlocker(...types) {
return this.create(...(types.map(type => `${type.name || type}-src ${type.value || "'none'"}`)));
},
blocks(header, type) {
return header.includes(`;${type}-src 'none';`)
},
types: ["script", "object", "media"],
};
const policyTypesMap = { const policyTypesMap = {
main_frame: "", main_frame: "",
sub_frame: "frame", sub_frame: "frame",
@ -58,8 +26,6 @@ var RequestGuard = (() => {
const allTypes = Object.keys(policyTypesMap); const allTypes = Object.keys(policyTypesMap);
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
const FORBID_DATAURI_TYPES = ["font", "media", "object"];
const TabStatus = { const TabStatus = {
map: new Map(), map: new Map(),
types: ["script", "object", "media", "frame", "font"], types: ["script", "object", "media", "frame", "font"],
@ -382,92 +348,66 @@ var RequestGuard = (() => {
} }
pending.headersProcessed = true; pending.headersProcessed = true;
let {url, documentUrl, statusCode, tabId, responseHeaders} = request; let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
let isMainFrame = type === "main_frame";
try { try {
let header, blocker; let header;
let content = {}; let content = {};
const REPORT_TO = csp.reportToHeader;
let hasReportTo = false; let hasReportTo = false;
for (let h of responseHeaders) { for (let h of responseHeaders) {
if (CSP.isMine(h)) { if (csp.isMine(h)) {
header = h; header = h;
h.value = CSP.inject(h.value, ""); h.value = csp.inject(h.value, "");
} else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) { } else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
content[RegExp.$1.toLowerCase()] = h.value; content[RegExp.$1.toLowerCase()] = h.value;
} else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) { } else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
hasReportTo = true; hasReportTo = true;
} }
} }
if (ns.isEnforced(tabId)) { if (ns.isEnforced(tabId)) {
let policy = ns.policy; let policy = ns.policy;
let perms = policy.get(url, documentUrl).perms; let perms = policy.get(url, documentUrl).perms;
if (policy.autoAllowTop && request.type === "main_frame" && perms === policy.DEFAULT) {
if (policy.autoAllowTop && isMainFrame && perms === policy.DEFAULT) {
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin); policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
await ChildPolicies.update(policy); await ChildPolicies.update(policy);
} }
let {capabilities} = perms; let blockHttp = !content.disposition &&
let isObject = request.type === "object"; (!content.type || /^\s*(?:video|audio|application)\//.test(content.type));
if (isObject && !capabilities.has("webgl")) { // we can't inject webglHook if (blockHttp) {
debug("Disabling scripts in object %s to prevent webgl abuse", url);
capabilities = new Set(capabilities);
capabilities.delete("script");
let r = Object.assign({}, request, {type: "webgl"});
TabStatus.record(r, "blocked");
Content.reportTo(r, false, "webgl");
}
let canScript = capabilities.has("script");
let blockedTypes;
let forbidData = new Set(FORBID_DATAURI_TYPES.filter(t => !capabilities.has(t)));
if (!content.disposition &&
(!content.type || /^\s*(?:video|audio|application)\//.test(content.type))) {
debug(`Suspicious content type "%s" in request %o with capabilities %o`, debug(`Suspicious content type "%s" in request %o with capabilities %o`,
content.type, request, capabilities); content.type, request, capabilities);
blockedTypes = new Set(CSP.types.filter(t => !capabilities.has(t)));
} else if(!canScript) {
blockedTypes = new Set(["script"]);
forbidData.add("object"); // data: URIs loaded in objects may run scripts
} else {
blockedTypes = new Set();
} }
for (let type of forbidData) { // object, font, media let blocker = csp.buildFromCapabilities(perms.capabilities, blockHttp);
if (blockedTypes.has(type)) continue; if (blocker) {
// HTTP is blocked in onBeforeRequest, let's allow it only and block if (!hasReportTo) {
// for instance data: and blob: URIs responseHeaders.push(csp.reportToHeader);
let dataBlocker = {name: type, value: "http: https:"}; }
blockedTypes.add(dataBlocker)
if (header) {
pending.cspHeader = header;
header.value = csp.inject(header.value, blocker);
} else {
header = csp.asHeader(blocker);
responseHeaders.push(header);
}
debug(`CSP blocker on %s:`, url, blocker, header.value);
} }
if (isMainFrame && !TabStatus.map.has(tabId)) {
if (blockedTypes.size) {
debug("Blocked types", blockedTypes);
blocker = CSP.createBlocker(...blockedTypes);
}
if (request.type === "main_frame" && !TabStatus.map.has(tabId)) {
debug("No TabStatus data yet for noscriptFrame", tabId); debug("No TabStatus data yet for noscriptFrame", tabId);
TabStatus.record(request, "noscriptFrame", true); TabStatus.record(request, "noscriptFrame", true);
} }
} }
debug(`CSP blocker on %s:`, url, blocker);
if (blocker) {
if (header) {
header.value = CSP.inject(header.value, blocker);
} else {
header = {name: CSP.name, value: blocker};
responseHeaders.push(header);
}
}
if (header) { if (header) {
if (blocker) pending.cspHeader = header;
if (!hasReportTo) {
responseHeaders.push(REPORT_TO);
}
return {responseHeaders}; return {responseHeaders};
} }
} catch (e) { } catch (e) {
@ -483,7 +423,7 @@ var RequestGuard = (() => {
TabStatus.initTab(tabId); TabStatus.initTab(tabId);
} }
let scriptBlocked = request.responseHeaders.some( let scriptBlocked = request.responseHeaders.some(
h => CSP.isMine(h) && CSP.blocks(h.value, "script") h => csp.isMine(h) && csp.blocks(h.value, "script")
); );
debug("%s scriptBlocked=%s setting noscriptFrame on ", url, scriptBlocked, tabId, frameId); debug("%s scriptBlocked=%s setting noscriptFrame on ", url, scriptBlocked, tabId, frameId);
TabStatus.record(request, "noscriptFrame", scriptBlocked); TabStatus.record(request, "noscriptFrame", scriptBlocked);
@ -608,7 +548,7 @@ var RequestGuard = (() => {
wr.onBeforeRequest.addListener(onViolationReport, wr.onBeforeRequest.addListener(onViolationReport,
{urls: [REPORT_URI], types: ["csp_report"]}, ["blocking", "requestBody"]); {urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
TabStatus.probe(); TabStatus.probe();
}, },

30
src/common/CapsCSP.js Normal file
View File

@ -0,0 +1,30 @@
"use strict";
function CapsCSP(baseCSP = new CSP()) {
return Object.assign(baseCSP, {
types: ["script", "object", "media"],
dataUriTypes: ["font", "media", "object"],
buildFromCapabilities(capabilities, netBlocker = false) {
let forbidData = new Set(this.dataUriTypes.filter(t => !capabilities.has(t)));
let blockedTypes;
if (netBlocker) {
blockedTypes = new Set(this.types.filter(t => !capabilities.has(t)));
} else if(!capabilities.has("script")) {
blockedTypes = new Set(["script"]);
forbidData.add("object"); // data: URIs loaded in objects may run scripts
} else {
blockedTypes = new Set();
}
for (let type of forbidData) {
if (blockedTypes.has(type)) continue;
// HTTP is blocked in onBeforeRequest, let's allow it only and block
// for instance data: and blob: URIs
let dataBlocker = {name: type, value: "http: https:"};
blockedTypes.add(dataBlocker)
}
return blockedTypes.size ? this.buildBlocker(...blockedTypes) : null;
}
});
}

22
src/lib/CSP.js Normal file
View File

@ -0,0 +1,22 @@
"use strict";
class CSP {
build(...directives) {
return directives.join(';');
}
buildBlocker(...types) {
return this.build(...(types.map(type => `${type.name || type}-src ${type.value || "'none'"}`)));
}
blocks(header, type) {
return `;${header};`.includes(`;${type}-src 'none';`)
}
asHeader(value) {
return {name: CSP.headerName, value};
}
}
CSP.headerName = "content-security-policy";

30
src/lib/NetCSP.js Normal file
View File

@ -0,0 +1,30 @@
"use strict";
class NetCSP extends CSP {
constructor(start, end) {
super();
this.start = start;
this.end = end;
}
isMine(header) {
let {name, value} = header;
if (name.toLowerCase() !== CSP.headerName) return false;
let startIdx = value.indexOf(this.start);
return startIdx > -1 && startIdx < value.lastIndexOf(this.end);
}
inject(headerValue, mine) {
let startIdx = headerValue.indexOf(this.start);
if (startIdx < 0) return `${headerValue};${mine}`;
let endIdx = headerValue.lastIndexOf(this.end);
let retValue = `${headerValue.substring(0, startIdx)}${mine}`;
return endIdx < 0 ? retValue : `${retValue}${headerValue.substring(endIdx + this.end.length + 1)}`;
}
build(...directives) {
return `${this.start}${super.build(...directives)}${this.end}`;
}
}

View File

@ -41,6 +41,9 @@
"lib/tld.js", "lib/tld.js",
"lib/LastListener.js", "lib/LastListener.js",
"lib/Messages.js", "lib/Messages.js",
"lib/CSP.js",
"lib/NetCSP.js",
"common/CapsCSP.js",
"common/Policy.js", "common/Policy.js",
"common/locale.js", "common/locale.js",
"common/Entities.js", "common/Entities.js",
@ -48,6 +51,7 @@
"common/Storage.js", "common/Storage.js",
"ui/Prompts.js", "ui/Prompts.js",
"xss/XSS.js", "xss/XSS.js",
"bg/ReportingCSP.js",
"bg/deferWebTraffic.js", "bg/deferWebTraffic.js",
"bg/ChildPolicies.js", "bg/ChildPolicies.js",
"bg/main.js" "bg/main.js"