summaryrefslogtreecommitdiff
path: root/src/bg/RequestGuard.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/bg/RequestGuard.js')
-rw-r--r--src/bg/RequestGuard.js218
1 files changed, 26 insertions, 192 deletions
diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js
index 7bdc929..5dea994 100644
--- a/src/bg/RequestGuard.js
+++ b/src/bg/RequestGuard.js
@@ -4,42 +4,7 @@ var RequestGuard = (() => {
browser.browserAction.setTitle({title: VERSION_LABEL});
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
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)}`;
- },
- 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"],
- };
-
+ let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
const policyTypesMap = {
main_frame: "",
sub_frame: "frame",
@@ -57,9 +22,6 @@ var RequestGuard = (() => {
};
const allTypes = Object.keys(policyTypesMap);
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
-
- const FORBID_DATAURI_TYPES = ["font", "media", "object"];
-
const TabStatus = {
map: new Map(),
types: ["script", "object", "media", "frame", "font"],
@@ -70,13 +32,11 @@ var RequestGuard = (() => {
noscriptFrames: {},
}
},
-
initTab(tabId, records = this.newRecords()) {
if (tabId < 0) return;
this.map.set(tabId, records);
return records;
},
-
_record(request, what, optValue) {
let {tabId, frameId, type, url, documentUrl} = request;
let policyType = policyTypesMap[type] || type;
@@ -88,7 +48,6 @@ var RequestGuard = (() => {
} else {
records = this.initTab(tabId);
}
-
if (what === "noscriptFrame" && type !== "object") {
let nsf = records.noscriptFrames;
nsf[frameId] = optValue;
@@ -110,7 +69,6 @@ var RequestGuard = (() => {
}
return records;
},
-
record(request, what, optValue) {
let {tabId} = request;
if (tabId < 0) return;
@@ -119,9 +77,7 @@ var RequestGuard = (() => {
this.updateTab(request.tabId);
}
},
-
_pendingTabs: new Set(),
-
updateTab(tabId) {
if (tabId < 0) return;
if (this._pendingTabs.size === 0) {
@@ -139,22 +95,18 @@ var RequestGuard = (() => {
let records = this.map.get(tabId) || this.initTab(tabId);
let {allowed, blocked, noscriptFrames} = records;
let topAllowed = !(noscriptFrames && noscriptFrames[0]);
-
let numAllowed = 0, numBlocked = 0, sum = 0;
let report = this.types.map(t => {
let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
numAllowed+= a, numBlocked += b, sum += s;
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
}).filter(s => s).join("\n");
-
let enforced = ns.isEnforced(tabId);
-
let icon = topAllowed ?
(numBlocked ? "part"
: enforced ? "yes" : "global")
: (numAllowed ? "sub" : "no");
let showBadge = ns.local.showCountBadge && numBlocked > 0;
-
let browserAction = browser.browserAction;
browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
@@ -165,11 +117,9 @@ var RequestGuard = (() => {
: _("NotEnforced")}`
});
},
-
totalize(sum, value) {
return sum + value;
},
-
async probe(tabId) {
if (tabId === undefined) {
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
@@ -181,7 +131,6 @@ var RequestGuard = (() => {
}
}
},
-
recordAll(tabId, seen) {
if (seen) {
let records = TabStatus.map.get(tabId);
@@ -190,17 +139,21 @@ var RequestGuard = (() => {
records.blocked = {};
}
for (let thing of seen) {
- thing.request.tabId = tabId;
- TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked");
+ let {request, allowed} = thing;
+ request.tabId = tabId;
+ debug(`Recording`, request);
+ TabStatus._record(request, allowed ? "allowed" : "blocked");
+ if (request.key === "noscript-probe" && request.type === "main_frame" ) {
+ request.frameId = 0;
+ TabStatus._record(request, "noscriptFrame", !allowed);
+ }
}
this._updateTabNow(tabId);
}
},
-
async onActivatedTab(info) {
let {tabId} = info;
let seen = await ns.collectSeen(tabId);
-
TabStatus.recordAll(tabId, seen);
},
onRemovedTab(tabId) {
@@ -209,12 +162,9 @@ var RequestGuard = (() => {
}
browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
-
if (!("setIcon" in browser.browserAction)) { // unsupported on Android
TabStatus._updateTabNow = TabStatus.updateTab = () => {};
}
-
-
let messageHandler = {
async pageshow(message, sender) {
TabStatus.recordAll(sender.tab.id, message.seen);
@@ -249,7 +199,6 @@ var RequestGuard = (() => {
if (!capabilities.has(policyType)) {
perms = new Permissions(new Set(capabilities), false);
perms.capabilities.add(policyType);
-
/* TODO: handle contextual permissions
if (documentUrl) {
let context = new URL(documentUrl).origin;
@@ -262,23 +211,8 @@ var RequestGuard = (() => {
}
return true;
},
-
- async queryDocStatus(message, sender) {
- let {frameId, tab} = sender;
- let {url} = message;
- let tabId = tab.id;
- let records = TabStatus.map.get(tabId);
- let noscriptFrames = records && records.noscriptFrames;
- let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]);
- let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script");
- debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript);
- return {canScript, shouldScript};
- }
-
}
-
const Content = {
-
async reportTo(request, allowed, policyType) {
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
let pending = pendingRequests.get(requestId); // null if from a CSP report
@@ -310,7 +244,6 @@ var RequestGuard = (() => {
}
}
};
-
const pendingRequests = new Map();
function initPendingRequest(request) {
let {requestId, url} = request;
@@ -322,8 +255,6 @@ var RequestGuard = (() => {
});
return redirected;
}
-
-
const ABORT = {cancel: true}, ALLOW = {};
const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/;
const listeners = {
@@ -341,7 +272,6 @@ var RequestGuard = (() => {
// livemark request or similar browser-internal, always allow;
return ALLOW;
}
-
if (/^(?:data|blob):/.test(url)) {
request._dataUrl = url;
request.url = url = documentUrl;
@@ -350,7 +280,6 @@ var RequestGuard = (() => {
!ns.isEnforced(request.tabId) ||
policy.can(url, policyType, originUrl);
Content.reportTo(request, allowed, policyType);
-
if (!allowed) {
debug(`Blocking ${policyType}`, request);
TabStatus.record(request, "blocked");
@@ -360,13 +289,10 @@ var RequestGuard = (() => {
} catch (e) {
error(e);
}
-
return ALLOW;
},
-
async onHeadersReceived(request) {
// called for main_frame, sub_frame and object
-
// check for duplicate calls
let pending = pendingRequests.get(request.requestId);
if (pending) {
@@ -381,93 +307,28 @@ var RequestGuard = (() => {
pending = pendingRequests.get(request.requestId);
}
pending.headersProcessed = true;
-
- let {url, documentUrl, statusCode, tabId, responseHeaders} = request;
-
+ let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
+ let isMainFrame = type === "main_frame";
try {
- let header, blocker;
- let content = {};
- let hasReportTo = false;
- for (let h of responseHeaders) {
- if (CSP.isMine(h)) {
- header = h;
- h.value = CSP.inject(h.value, "");
- } else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
- content[RegExp.$1.toLowerCase()] = h.value;
- } else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
- hasReportTo = true;
- }
- }
-
+ let capabilities;
if (ns.isEnforced(tabId)) {
let policy = ns.policy;
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);
await ChildPolicies.update(policy);
}
-
- let {capabilities} = perms;
- let isObject = request.type === "object";
- if (isObject && !capabilities.has("webgl")) { // we can't inject webglHook
- 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`,
- 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
- 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 (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);
- TabStatus.record(request, "noscriptFrame", true);
- }
+ capabilities = perms.capabilities;
}
-
- 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 (isMainFrame && !TabStatus.map.has(tabId)) {
+ debug("No TabStatus data yet for noscriptFrame", tabId);
+ TabStatus.record(request, "noscriptFrame",
+ capabilities && !capabilities.has("script"));
}
-
+ let header = csp.patchHeaders(responseHeaders, capabilities);
if (header) {
- if (blocker) pending.cspHeader = header;
- if (!hasReportTo) {
- responseHeaders.push(REPORT_TO);
- }
+ pending.cspHeader = header;
+ debug(`CSP blocker on %s:`, url, header.value);
return {responseHeaders};
}
} catch (e) {
@@ -475,7 +336,6 @@ var RequestGuard = (() => {
}
return ALLOW;
},
-
onResponseStarted(request) {
debug("onResponseStarted", request);
let {requestId, url, tabId, frameId, type} = request;
@@ -483,32 +343,22 @@ var RequestGuard = (() => {
TabStatus.initTab(tabId);
}
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);
TabStatus.record(request, "noscriptFrame", scriptBlocked);
let pending = pendingRequests.get(requestId);
if (pending) {
pending.scriptBlocked = scriptBlocked;
- if (!(pending.headersProcessed &&
+ if (!(pending.headersProcessed &&
(scriptBlocked || !ns.isEnforced(tabId) || ns.policy.can(url, "script", request.documentURL))
)) {
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
- pending.headersProcessed ? "has been overridden on": "could not process",
+ pending.headersProcessed ? "has been overridden on": "could not process",
request);
-
- if (tabId !== -1 && type !== "object") {
- debug("[WARNING] Reloading %s frame %s of tab %s.", url, frameId, tabId);
- browser.tabs.executeScript(tabId, {
- runAt: "document_start",
- code: "window.location.reload(false)",
- frameId
- });
- }
}
}
},
-
onCompleted(request) {
let {requestId} = request;
if (pendingRequests.has(requestId)) {
@@ -523,12 +373,10 @@ var RequestGuard = (() => {
}
}
},
-
onErrorOccurred(request) {
pendingRequests.delete(request.requestId);
}
};
-
function fakeRequestFromCSP(report, request) {
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
if (type === "frame") type = "sub_frame";
@@ -539,7 +387,6 @@ var RequestGuard = (() => {
type,
});
}
-
async function onViolationReport(request) {
try {
let decoder = new TextDecoder("UTF-8");
@@ -561,28 +408,21 @@ var RequestGuard = (() => {
}
return ABORT;
}
-
const RequestGuard = {
async start() {
Messages.addHandler(messageHandler);
-
let wr = browser.webRequest;
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
-
let allUrls = ["<all_urls>"];
let docTypes = ["main_frame", "sub_frame", "object"];
-
let filterDocs = {urls: allUrls, types: docTypes};
let filterAll = {urls: allUrls, types: allTypes};
-
listen("onBeforeRequest", filterAll, ["blocking"]);
-
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
-
(listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => {
let {requestId, responseHeaders} = request;
let pending = pendingRequests.get(request.requestId);
- if (pending && pending.headersProcessed) {
+ if (pending && pending.headersProcessed) {
let {cspHeader} = pending;
if (cspHeader) {
debug("Safety net: injecting again %o in %o", cspHeader, request);
@@ -601,18 +441,13 @@ var RequestGuard = (() => {
}
return null;
}, filterDocs, ["blocking", "responseHeaders"])).install();
-
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll);
-
-
wr.onBeforeRequest.addListener(onViolationReport,
- {urls: [REPORT_URI], types: ["csp_report"]}, ["blocking", "requestBody"]);
-
+ {urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
TabStatus.probe();
},
-
stop() {
let wr = browser.webRequest;
for (let [name, listener] of Object.entries(listeners)) {
@@ -626,6 +461,5 @@ var RequestGuard = (() => {
Messages.removeHandler(messageHandler);
}
};
-
return RequestGuard;
})();