Refactoring CSP building out of RequestGuard.
This commit is contained in:
parent
b5d7266c50
commit
e82e961dd7
|
@ -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 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,43 +2,11 @@ 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",
|
|
||||||
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)}`;
|
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
|
||||||
},
|
|
||||||
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: "",
|
||||||
|
@ -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,16 +348,19 @@ 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) {
|
||||||
|
@ -402,72 +371,43 @@ var RequestGuard = (() => {
|
||||||
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"]);
|
let blocker = csp.buildFromCapabilities(perms.capabilities, blockHttp);
|
||||||
forbidData.add("object"); // data: URIs loaded in objects may run scripts
|
if (blocker) {
|
||||||
|
if (!hasReportTo) {
|
||||||
|
responseHeaders.push(csp.reportToHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
pending.cspHeader = header;
|
||||||
|
header.value = csp.inject(header.value, blocker);
|
||||||
} else {
|
} else {
|
||||||
blockedTypes = new Set();
|
header = csp.asHeader(blocker);
|
||||||
|
responseHeaders.push(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let type of forbidData) { // object, font, media
|
debug(`CSP blocker on %s:`, url, blocker, header.value);
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (header) {
|
||||||
header.value = CSP.inject(header.value, blocker);
|
|
||||||
} else {
|
|
||||||
header = {name: CSP.name, value: blocker};
|
|
||||||
responseHeaders.push(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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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";
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue