Further CSP refactoring and removal of obsolete fallbacks.

This commit is contained in:
hackademix 2018-08-27 00:31:37 +02:00
parent 6e80d3f130
commit e2b63cf982
16 changed files with 339 additions and 362 deletions

View File

@ -1,6 +1,7 @@
"use strict"; "use strict";
{ {
let marker = JSON.stringify(uuid()); let marker = JSON.stringify(uuid());
let allUrls = ["<all_urls>"];
let Scripts = { let Scripts = {
references: new Set(), references: new Set(),
@ -10,27 +11,52 @@
matchAboutBlank: true, matchAboutBlank: true,
runAt: "document_start" runAt: "document_start"
}, },
async init() {
let opts = Object.assign({}, this.opts);
opts.js = [{file: "/content/dynamicNS.js"}];
opts.matches = allUrls;
delete opts.excludedMatches;
this._stubScript = await browser.contentScripts.register(opts);
this.init = this.forget;
},
forget() { forget() {
for (let script of [...this.references]) { for (let script of [...this.references]) {
script.unregister(); script.unregister();
this.references.delete(script); this.references.delete(script);
} }
}, },
debug: false,
trace(code) {
return this.debug
? `console.debug("Executing child policy", ${JSON.stringify(code)});${code}`
: code
;
},
async register(code, matches, excludeMatches) { async register(code, matches, excludeMatches) {
debug("Registering child policy.", code, matches, excludeMatches); debug("Registering child policy.", code, matches, excludeMatches);
if (!matches.length) return; if (!matches.length) return;
try { try {
this.opts.js[0].code = code; let opts = Object.assign({}, this.opts);
this.opts.matches = matches; opts.js[0].code = this.trace(code);
opts.matches = matches;
if (excludeMatches && excludeMatches.length) { if (excludeMatches && excludeMatches.length) {
this.opts.excludeMatches = excludeMatches; opts.excludeMatches = excludeMatches;
} else {
delete this.opts.excludeMatches;
} }
this.references.add(await browser.contentScripts.register(this.opts)); this.references.add(await browser.contentScripts.register(opts));
} catch (e) { } catch (e) {
error(e); error(e);
} }
},
buildPerms(perms, finalizeSetup = false) {
if (typeof perms !== "string") {
perms = JSON.stringify(perms);
}
return finalizeSetup
? `ns.setup(${perms}, ${marker});`
: `ns.config.CURRENT = ${perms};`
;
} }
}; };
@ -69,12 +95,13 @@
error(e); error(e);
} }
}, },
async update(policy) { async update(policy, debug) {
Scripts.forget(); if (debug !== "undefined") Scripts.debug = debug;
await Scripts.init();
if (!policy.enforced) { if (!policy.enforced) {
await Scripts.register(`ns.setup(null, ${marker});`, await Scripts.register(`ns.setup(null, ${marker});`, allUrls);
["<all_urls>"]);
return; return;
} }
@ -122,10 +149,27 @@
// register new content scripts // register new content scripts
for (let [perms, keys] of [...permsMap]) { for (let [perms, keys] of [...permsMap]) {
await Scripts.register(`ns.perms.CURRENT = ${perms};`, siteKeys2MatchPatterns(keys), excludeMap.get(perms)); await Scripts.register(Scripts.buildPerms(perms), siteKeys2MatchPatterns(keys), excludeMap.get(perms));
}
await Scripts.register(`ns.setup(${JSON.stringify(serialized.DEFAULT)}, ${marker});`,
["<all_urls>"]);
} }
await Scripts.register(Scripts.buildPerms(serialized.DEFAULT, true), allUrls);
},
getForDocument(policy, url, context = null) {
return {
CURRENT: policy.get(url, context).perms.dry(),
DEFAULT: policy.DEFAULT.dry(),
MARKER: marker
};
},
async updateFrame(tabId, frameId, perms, defaultPreset) {
let code = Scripts.buildPerms(perms) + Scripts.buildPerms(defaultPreset, true);
await browser.tabs.executeScript(tabId, {
code,
frameId,
matchAboutBlank: true,
runAt: "document_start"
});
} }
};
} }

View File

@ -1,6 +1,12 @@
"use strict"; "use strict";
function ReportingCSP(reportURI, reportGroup) { function ReportingCSP(reportURI, reportGroup) {
const REPORT_TO = {
name: "Report-To",
value: JSON.stringify({ "url": reportURI,
"group": reportGroup,
"max-age": 10886400 }),
};
return Object.assign( return Object.assign(
new CapsCSP(new NetCSP( new CapsCSP(new NetCSP(
`report-uri ${reportURI};`, `report-uri ${reportURI};`,
@ -9,11 +15,32 @@ function ReportingCSP(reportURI, reportGroup) {
{ {
reportURI, reportURI,
reportGroup, reportGroup,
reportToHeader: { patchHeaders(responseHeaders, capabilities) {
name: "Report-To", let header = null;
value: JSON.stringify({ "url": reportURI, let hasReportTo = false;
"group": reportGroup, for (let h of responseHeaders) {
"max-age": 10886400 }), if (this.isMine(h)) {
header = h;
h.value = this.inject(h.value, "");
} else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
hasReportTo = true;
}
}
let blocker = capabilities && this.buildFromCapabilities(capabilities);
if (blocker) {
if (!hasReportTo) {
responseHeaders.push(REPORT_TO);
}
if (header) {
header.value = this.inject(header.value, blocker);
} else {
header = this.asHeader(blocker);
responseHeaders.push(header);
}
}
return header;
} }
} }
); );

View File

@ -2,12 +2,9 @@ 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";
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP); let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
const policyTypesMap = { const policyTypesMap = {
main_frame: "", main_frame: "",
sub_frame: "frame", sub_frame: "frame",
@ -25,7 +22,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 TabStatus = { const TabStatus = {
map: new Map(), map: new Map(),
types: ["script", "object", "media", "frame", "font"], types: ["script", "object", "media", "frame", "font"],
@ -36,13 +32,11 @@ var RequestGuard = (() => {
noscriptFrames: {}, noscriptFrames: {},
} }
}, },
initTab(tabId, records = this.newRecords()) { initTab(tabId, records = this.newRecords()) {
if (tabId < 0) return; if (tabId < 0) return;
this.map.set(tabId, records); this.map.set(tabId, records);
return records; return records;
}, },
_record(request, what, optValue) { _record(request, what, optValue) {
let {tabId, frameId, type, url, documentUrl} = request; let {tabId, frameId, type, url, documentUrl} = request;
let policyType = policyTypesMap[type] || type; let policyType = policyTypesMap[type] || type;
@ -54,7 +48,6 @@ var RequestGuard = (() => {
} else { } else {
records = this.initTab(tabId); records = this.initTab(tabId);
} }
if (what === "noscriptFrame" && type !== "object") { if (what === "noscriptFrame" && type !== "object") {
let nsf = records.noscriptFrames; let nsf = records.noscriptFrames;
nsf[frameId] = optValue; nsf[frameId] = optValue;
@ -76,7 +69,6 @@ var RequestGuard = (() => {
} }
return records; return records;
}, },
record(request, what, optValue) { record(request, what, optValue) {
let {tabId} = request; let {tabId} = request;
if (tabId < 0) return; if (tabId < 0) return;
@ -85,9 +77,7 @@ var RequestGuard = (() => {
this.updateTab(request.tabId); this.updateTab(request.tabId);
} }
}, },
_pendingTabs: new Set(), _pendingTabs: new Set(),
updateTab(tabId) { updateTab(tabId) {
if (tabId < 0) return; if (tabId < 0) return;
if (this._pendingTabs.size === 0) { if (this._pendingTabs.size === 0) {
@ -105,22 +95,18 @@ var RequestGuard = (() => {
let records = this.map.get(tabId) || this.initTab(tabId); let records = this.map.get(tabId) || this.initTab(tabId);
let {allowed, blocked, noscriptFrames} = records; let {allowed, blocked, noscriptFrames} = records;
let topAllowed = !(noscriptFrames && noscriptFrames[0]); let topAllowed = !(noscriptFrames && noscriptFrames[0]);
let numAllowed = 0, numBlocked = 0, sum = 0; let numAllowed = 0, numBlocked = 0, sum = 0;
let report = this.types.map(t => { let report = this.types.map(t => {
let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b; let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
numAllowed+= a, numBlocked += b, sum += s; numAllowed+= a, numBlocked += b, sum += s;
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`; return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
}).filter(s => s).join("\n"); }).filter(s => s).join("\n");
let enforced = ns.isEnforced(tabId); let enforced = ns.isEnforced(tabId);
let icon = topAllowed ? let icon = topAllowed ?
(numBlocked ? "part" (numBlocked ? "part"
: enforced ? "yes" : "global") : enforced ? "yes" : "global")
: (numAllowed ? "sub" : "no"); : (numAllowed ? "sub" : "no");
let showBadge = ns.local.showCountBadge && numBlocked > 0; let showBadge = ns.local.showCountBadge && numBlocked > 0;
let browserAction = browser.browserAction; let browserAction = browser.browserAction;
browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}}); browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""}); browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
@ -131,11 +117,9 @@ var RequestGuard = (() => {
: _("NotEnforced")}` : _("NotEnforced")}`
}); });
}, },
totalize(sum, value) { totalize(sum, value) {
return sum + value; return sum + value;
}, },
async probe(tabId) { async probe(tabId) {
if (tabId === undefined) { if (tabId === undefined) {
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id)); (await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
@ -147,7 +131,6 @@ var RequestGuard = (() => {
} }
} }
}, },
recordAll(tabId, seen) { recordAll(tabId, seen) {
if (seen) { if (seen) {
let records = TabStatus.map.get(tabId); let records = TabStatus.map.get(tabId);
@ -156,17 +139,21 @@ var RequestGuard = (() => {
records.blocked = {}; records.blocked = {};
} }
for (let thing of seen) { for (let thing of seen) {
thing.request.tabId = tabId; let {request, allowed} = thing;
TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked"); 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); this._updateTabNow(tabId);
} }
}, },
async onActivatedTab(info) { async onActivatedTab(info) {
let {tabId} = info; let {tabId} = info;
let seen = await ns.collectSeen(tabId); let seen = await ns.collectSeen(tabId);
TabStatus.recordAll(tabId, seen); TabStatus.recordAll(tabId, seen);
}, },
onRemovedTab(tabId) { onRemovedTab(tabId) {
@ -175,12 +162,9 @@ var RequestGuard = (() => {
} }
browser.tabs.onActivated.addListener(TabStatus.onActivatedTab); browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab); browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
if (!("setIcon" in browser.browserAction)) { // unsupported on Android if (!("setIcon" in browser.browserAction)) { // unsupported on Android
TabStatus._updateTabNow = TabStatus.updateTab = () => {}; TabStatus._updateTabNow = TabStatus.updateTab = () => {};
} }
let messageHandler = { let messageHandler = {
async pageshow(message, sender) { async pageshow(message, sender) {
TabStatus.recordAll(sender.tab.id, message.seen); TabStatus.recordAll(sender.tab.id, message.seen);
@ -215,7 +199,6 @@ var RequestGuard = (() => {
if (!capabilities.has(policyType)) { if (!capabilities.has(policyType)) {
perms = new Permissions(new Set(capabilities), false); perms = new Permissions(new Set(capabilities), false);
perms.capabilities.add(policyType); perms.capabilities.add(policyType);
/* TODO: handle contextual permissions /* TODO: handle contextual permissions
if (documentUrl) { if (documentUrl) {
let context = new URL(documentUrl).origin; let context = new URL(documentUrl).origin;
@ -228,23 +211,8 @@ var RequestGuard = (() => {
} }
return true; 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 = { const Content = {
async reportTo(request, allowed, policyType) { async reportTo(request, allowed, policyType) {
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request; let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
let pending = pendingRequests.get(requestId); // null if from a CSP report let pending = pendingRequests.get(requestId); // null if from a CSP report
@ -276,7 +244,6 @@ var RequestGuard = (() => {
} }
} }
}; };
const pendingRequests = new Map(); const pendingRequests = new Map();
function initPendingRequest(request) { function initPendingRequest(request) {
let {requestId, url} = request; let {requestId, url} = request;
@ -288,8 +255,6 @@ var RequestGuard = (() => {
}); });
return redirected; return redirected;
} }
const ABORT = {cancel: true}, ALLOW = {}; const ABORT = {cancel: true}, ALLOW = {};
const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/; const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/;
const listeners = { const listeners = {
@ -307,7 +272,6 @@ var RequestGuard = (() => {
// livemark request or similar browser-internal, always allow; // livemark request or similar browser-internal, always allow;
return ALLOW; return ALLOW;
} }
if (/^(?:data|blob):/.test(url)) { if (/^(?:data|blob):/.test(url)) {
request._dataUrl = url; request._dataUrl = url;
request.url = url = documentUrl; request.url = url = documentUrl;
@ -316,7 +280,6 @@ var RequestGuard = (() => {
!ns.isEnforced(request.tabId) || !ns.isEnforced(request.tabId) ||
policy.can(url, policyType, originUrl); policy.can(url, policyType, originUrl);
Content.reportTo(request, allowed, policyType); Content.reportTo(request, allowed, policyType);
if (!allowed) { if (!allowed) {
debug(`Blocking ${policyType}`, request); debug(`Blocking ${policyType}`, request);
TabStatus.record(request, "blocked"); TabStatus.record(request, "blocked");
@ -326,13 +289,10 @@ var RequestGuard = (() => {
} catch (e) { } catch (e) {
error(e); error(e);
} }
return ALLOW; return ALLOW;
}, },
async onHeadersReceived(request) { async onHeadersReceived(request) {
// called for main_frame, sub_frame and object // called for main_frame, sub_frame and object
// check for duplicate calls // check for duplicate calls
let pending = pendingRequests.get(request.requestId); let pending = pendingRequests.get(request.requestId);
if (pending) { if (pending) {
@ -347,67 +307,28 @@ var RequestGuard = (() => {
pending = pendingRequests.get(request.requestId); pending = pendingRequests.get(request.requestId);
} }
pending.headersProcessed = true; pending.headersProcessed = true;
let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request; let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
let isMainFrame = type === "main_frame"; let isMainFrame = type === "main_frame";
try { try {
let header; let capabilities;
let content = {};
const REPORT_TO = csp.reportToHeader;
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;
}
}
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 && isMainFrame && 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);
} }
capabilities = perms.capabilities;
let blockHttp = !content.disposition &&
(!content.type || /^\s*(?:video|audio|application)\//.test(content.type));
if (blockHttp) {
debug(`Suspicious content type "%s" in request %o with capabilities %o`,
content.type, request, capabilities);
} }
let blocker = csp.buildFromCapabilities(perms.capabilities, blockHttp);
if (blocker) {
if (!hasReportTo) {
responseHeaders.push(csp.reportToHeader);
}
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 (isMainFrame && !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",
capabilities && !capabilities.has("script"));
} }
} let header = csp.patchHeaders(responseHeaders, capabilities);
if (header) { if (header) {
pending.cspHeader = header;
debug(`CSP blocker on %s:`, url, header.value);
return {responseHeaders}; return {responseHeaders};
} }
} catch (e) { } catch (e) {
@ -415,7 +336,6 @@ var RequestGuard = (() => {
} }
return ALLOW; return ALLOW;
}, },
onResponseStarted(request) { onResponseStarted(request) {
debug("onResponseStarted", request); debug("onResponseStarted", request);
let {requestId, url, tabId, frameId, type} = request; let {requestId, url, tabId, frameId, type} = request;
@ -436,19 +356,9 @@ var RequestGuard = (() => {
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId, 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); 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) { onCompleted(request) {
let {requestId} = request; let {requestId} = request;
if (pendingRequests.has(requestId)) { if (pendingRequests.has(requestId)) {
@ -463,12 +373,10 @@ var RequestGuard = (() => {
} }
} }
}, },
onErrorOccurred(request) { onErrorOccurred(request) {
pendingRequests.delete(request.requestId); pendingRequests.delete(request.requestId);
} }
}; };
function fakeRequestFromCSP(report, request) { function fakeRequestFromCSP(report, request) {
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
if (type === "frame") type = "sub_frame"; if (type === "frame") type = "sub_frame";
@ -479,7 +387,6 @@ var RequestGuard = (() => {
type, type,
}); });
} }
async function onViolationReport(request) { async function onViolationReport(request) {
try { try {
let decoder = new TextDecoder("UTF-8"); let decoder = new TextDecoder("UTF-8");
@ -501,24 +408,17 @@ var RequestGuard = (() => {
} }
return ABORT; return ABORT;
} }
const RequestGuard = { const RequestGuard = {
async start() { async start() {
Messages.addHandler(messageHandler); Messages.addHandler(messageHandler);
let wr = browser.webRequest; let wr = browser.webRequest;
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args); let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
let allUrls = ["<all_urls>"]; let allUrls = ["<all_urls>"];
let docTypes = ["main_frame", "sub_frame", "object"]; let docTypes = ["main_frame", "sub_frame", "object"];
let filterDocs = {urls: allUrls, types: docTypes}; let filterDocs = {urls: allUrls, types: docTypes};
let filterAll = {urls: allUrls, types: allTypes}; let filterAll = {urls: allUrls, types: allTypes};
listen("onBeforeRequest", filterAll, ["blocking"]); listen("onBeforeRequest", filterAll, ["blocking"]);
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]); listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
(listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => { (listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => {
let {requestId, responseHeaders} = request; let {requestId, responseHeaders} = request;
let pending = pendingRequests.get(request.requestId); let pending = pendingRequests.get(request.requestId);
@ -541,18 +441,13 @@ var RequestGuard = (() => {
} }
return null; return null;
}, filterDocs, ["blocking", "responseHeaders"])).install(); }, filterDocs, ["blocking", "responseHeaders"])).install();
listen("onResponseStarted", filterDocs, ["responseHeaders"]); listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll); listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll); listen("onErrorOccurred", filterAll);
wr.onBeforeRequest.addListener(onViolationReport, wr.onBeforeRequest.addListener(onViolationReport,
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]); {urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
TabStatus.probe(); TabStatus.probe();
}, },
stop() { stop() {
let wr = browser.webRequest; let wr = browser.webRequest;
for (let [name, listener] of Object.entries(listeners)) { for (let [name, listener] of Object.entries(listeners)) {
@ -566,6 +461,5 @@ var RequestGuard = (() => {
Messages.removeHandler(messageHandler); Messages.removeHandler(messageHandler);
} }
}; };
return RequestGuard; return RequestGuard;
})(); })();

View File

@ -23,10 +23,13 @@
} }
async function init() { async function init() {
await include("/bg/defaults.js");
await ns.defaults;
let policyData = (await Storage.get("sync", "policy")).policy; let policyData = (await Storage.get("sync", "policy")).policy;
if (policyData && policyData.DEFAULT) { if (policyData && policyData.DEFAULT) {
ns.policy = new Policy(policyData); ns.policy = new Policy(policyData);
await ChildPolicies.update(policyData); await ChildPolicies.update(policyData, ns.local.debug);
} else { } else {
await include("/legacy/Legacy.js"); await include("/legacy/Legacy.js");
ns.policy = await Legacy.createOrMigratePolicy(); ns.policy = await Legacy.createOrMigratePolicy();
@ -34,8 +37,7 @@
} }
await include("/bg/defaults.js");
await ns.defaults;
await include("/bg/RequestGuard.js"); await include("/bg/RequestGuard.js");
await RequestGuard.start(); await RequestGuard.start();
await XSS.start(); // we must start it anyway to initialize sub-objects await XSS.start(); // we must start it anyway to initialize sub-objects
@ -136,6 +138,10 @@
return await Settings.import(data); return await Settings.import(data);
}, },
async fetchChildPolicy({url, contextUrl}) {
return ChildPolicies.getForDocument(ns.policy, url, contextUrl);
},
async openStandalonePopup() { async openStandalonePopup() {
let win = await browser.windows.getLastFocused(); let win = await browser.windows.getLastFocused();
let [tab] = (await browser.tabs.query({ let [tab] = (await browser.tabs.query({
@ -203,7 +209,7 @@
async savePolicy() { async savePolicy() {
if (this.policy) { if (this.policy) {
await ChildPolicies.update(this.policy); await ChildPolicies.update(this.policy, this.local.debug);
await Storage.set("sync", { await Storage.set("sync", {
policy: this.policy.dry() policy: this.policy.dry()
}); });

View File

@ -6,9 +6,9 @@ class DocumentCSP {
this.builder = new CapsCSP(); this.builder = new CapsCSP();
} }
apply(capabilities) { apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) {
let csp = this.builder; let csp = this.builder;
let blocker = csp.buildFromCapabilities(capabilities); let blocker = csp.buildFromCapabilities(capabilities, embedding);
if (!blocker) return; if (!blocker) return;
let document = this.document; let document = this.document;
@ -19,8 +19,11 @@ class DocumentCSP {
let parent = document.head || document.documentElement; let parent = document.head || document.documentElement;
try { try {
parent.insertBefore(meta, parent.firstChild); parent.insertBefore(meta, parent.firstChild);
debug(`Failsafe <meta> CSP inserted in the DOM: "%s"`, header.value);
if (capabilities.has("script")) meta.remove();
} catch (e) { } catch (e) {
error(e, "Error inserting CSP %s in the DOM", header && header.value); error(e, "Error inserting CSP %s in the DOM", header && header.value);
} }
} }
} }

View File

@ -1,6 +1,17 @@
var PlaceHolder = (() => { var PlaceHolder = (() => {
const HANDLERS = new Map(); const HANDLERS = new Map();
let checkStyle = async () => {
checkStyle = () => {};
if (!ns.embeddingDocument) return;
let replacement = document.querySelector("a.__NoScript_PlaceHolder__");
if (!replacement) return;
if (window.getComputedStyle(replacement, null).opacity !== "0.8") {
document.head.appendChild(createHTMLElement("style")).textContent = await
(await fetch(browser.extension.getURL("/content/content.css"))).text();
}
}
class Handler { class Handler {
constructor(type, selector) { constructor(type, selector) {
this.type = type; this.type = type;
@ -9,6 +20,7 @@ var PlaceHolder = (() => {
HANDLERS.set(type, this); HANDLERS.set(type, this);
} }
filter(element, request) { filter(element, request) {
if (request.embeddingDocument) return true;
let url = request.initialUrl || request.url; let url = request.initialUrl || request.url;
return "data" in element ? element.data === url : element.src === url; return "data" in element ? element.data === url : element.src === url;
} }
@ -77,10 +89,14 @@ var PlaceHolder = (() => {
.filter(element => this.handler.filter(element, request)) .filter(element => this.handler.filter(element, request))
.forEach(element => this.replace(element)); .forEach(element => this.replace(element));
}; };
if (this.replacements.size) PlaceHolder.listen(); if (this.replacements.size) {
PlaceHolder.listen();
checkStyle();
}
} }
replace(element) { replace(element) {
if (!element.parentElement) return;
let { let {
url url
} = this.request; } = this.request;
@ -108,10 +124,10 @@ var PlaceHolder = (() => {
replacement._placeHolderObj = this; replacement._placeHolderObj = this;
replacement._placeHolderElement = element; replacement._placeHolderElement = element;
this.replacements.add(replacement);
if (element.parentNode) element.parentNode.replaceChild(replacement, element);
else document.body.appendChild(replacement); element.parentNode.replaceChild(replacement, element);
this.replacements.add(replacement);
} }
async enable(replacement) { async enable(replacement) {

View File

@ -1,112 +1,12 @@
'use strict'; 'use strict';
// debug = () => {}; // REL_ONLY
// debug = () => {}; // REL_ONLY var _ = browser.i18n.getMessage;
{
let listenersMap = new Map();
let backlog = new Set();
var ns = {
on(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (!listeners) listenersMap.set(eventName, listeners = new Set());
listeners.add(listener);
if (backlog.has(eventName)) this.fire(eventName, listener);
},
detach(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (listeners) listeners.delete(listener);
},
fire(eventName, listener = null) {
if (listener) {
listener({type:eventName, source: this});
return;
}
let listeners = listenersMap.get(eventName);
if (listeners) {
for (let l of listeners) {
this.fire(eventName, l);
}
}
backlog.add(eventName);
},
setup(DEFAULT, MARKER) {
this.perms.DEFAULT = DEFAULT;
if(!this.perms.CURRENT) this.perms.CURRENT = DEFAULT;
// ugly hack: since now we use registerContentScript instead of the
// filterRequest dynamic script injection hack, we use top.name
// to store per-tab information. We don't want web content to
// mess with it, though, so we wrap it around auto-hiding accessors
this.perms.MARKER = MARKER;
let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
if (eraseTabInfoRx.test(top.name)) {
let _name = top.name;
let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
if (top === window) { // wrap to hide
Reflect.defineProperty(top.wrappedJSObject, "name", {
get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
set: exportFunction(value => {
let preamble = top.name.match(tabInfoRx);
top.name = `${preamble && preamble[0] || ""}${value}`;
return value;
}, top.wrappedJSObject)
});
}
let tabInfoMatch = _name.match(tabInfoRx);
if (tabInfoMatch) try {
this.perms.tabInfo = JSON.parse(tabInfoMatch[1]);
} catch (e) {
error(e);
}
}
if (!this.perms.DEFAULT || this.perms.tabInfo.unrestricted) {
this.allows = () => true;
this.capabilities = Object.assign(
new Set(["script"]), { has() { return true; } });
} else {
let perms = this.perms.CURRENT || this.perms.DEFAULT;
this.capabilities = new Set(perms.capabilities);
new DocumentCSP(document).apply(this.capabilities);
}
ns.fire("perms");
},
perms: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
allows(cap) {
return this.capabilities && this.capabilities.has(cap);
},
getWindowName() {
return top !== window || !this.perms.MARKER ? window.name
: window.name.split(this.perms.MARKER + ",").pop();
}
}
}
var canScript = true, shouldScript = false;
let now = () => performance.now() + performance.timeOrigin;
function createHTMLElement(name) { function createHTMLElement(name) {
return document.createElementNS("http://www.w3.org/1999/xhtml", name); return document.createElementNS("http://www.w3.org/1999/xhtml", name);
} }
function probe() {
try {
debug("Probing execution...");
let s = document.createElement("script");
s.textContent = ";";
document.documentElement.appendChild(s);
s.remove();
} catch(e) {
debug(e);
}
}
var _ = browser.i18n.getMessage;
var embeddingDocument = false;
var seen = { var seen = {
_map: new Map(), _map: new Map(),
_list: null, _list: null,
@ -128,9 +28,8 @@ Messages.addHandler({
seen.record(event); seen.record(event);
} }
if (ownFrame) { if (ownFrame) {
init();
if (!allowed && PlaceHolder.canReplace(policyType)) { if (!allowed && PlaceHolder.canReplace(policyType)) {
request.embeddingDocument = embeddingDocument; request.embeddingDocument = ns.embeddingDocument;
PlaceHolder.create(policyType, request); PlaceHolder.create(policyType, request);
} }
} }
@ -142,87 +41,38 @@ Messages.addHandler({
} }
}); });
if (document.readyState !== "complete") {
let pageshown = e => { debug(`Loading NoScript in document %s, scripting=%s, readyState %s`,
removeEventListener("pageshow", pageshown); document.URL, ns.canScript, document.readyState);
init();
}; var notifyPage = async () => {
addEventListener("pageshow", pageshown);
} else {
init(true);
}
let notifyPage = async () => {
debug("Page %s shown, %s", document.URL, document.readyState); debug("Page %s shown, %s", document.URL, document.readyState);
if (document.readyState === "complete") { if (document.readyState === "complete") {
try { try {
await Messages.send("pageshow", {seen: seen.list, canScript}); if (!("canScript" in ns)) {
let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL, contextUrl: top.location.href});
ns.config.CURRENT = childPolicy.CURRENT;
ns.setup(childPolicy.DEFAULT, childPolicy.MARKER);
return;
}
await Messages.send("pageshow", {seen: seen.list, canScript: ns.canScript});
return true; return true;
} catch (e) { } catch (e) {
debug(e); debug(e);
if (/Receiving end does not exist/.test(e.message)) {
window.setTimeout(notifyPage, 2000);
}
} }
} }
return false; return false;
} }
var queryingStatus = false; notifyPage();
function reload(noCache = false) { window.addEventListener("pageshow", notifyPage);
init = () => {};
location.reload(noCache);
}
async function init(oldPage = false) { ns.on("capabilities", () => {
if (queryingStatus) return;
if (!document.URL.startsWith("http")) {
return;
}
queryingStatus = true;
debug(`init() called in document %s, contentType %s readyState %s, frameElement %o`,
document.URL, document.contentType, document.readyState, window.frameElement && frameElement.data);
try {
({canScript, shouldScript} = await Messages.send("queryDocStatus", {url: document.URL}));
debug(`document %s, canScript=%s, shouldScript=%s, readyState %s`, document.URL, canScript, shouldScript, document.readyState);
if (canScript) {
if (oldPage) {
probe();
return;
}
if (!shouldScript &&
(document.readyState !== "complete" ||
now() - performance.timing.domContentLoadedEventStart < 5000)) {
// Something wrong: scripts can run, permissions say they shouldn't.
// Was webRequest bypassed by caching/session restore/service workers?
window.stop();
let noCache = !!navigator.serviceWorker.controller;
if (noCache) {
for (let r of await navigator.serviceWorker.getRegistrations()) {
await r.unregister();
}
}
debug("Reloading %s (%s)", document.URL, noCache ? "no cache" : "cached");
reload(noCache);
return;
}
}
init = () => {};
} catch (e) {
debug("Error querying docStatus", e);
if (!oldPage &&
/Receiving end does not exist/.test(e.message)) {
// probably startup and bg page not ready yet, hence no CSP: reload!
debug("Reloading", document.URL);
reload();
} else {
setTimeout(() => init(oldPage), 100);
}
return;
} finally {
queryingStatus = false;
}
if (!canScript) onScriptDisabled();
seen.record({ seen.record({
request: { request: {
key: "noscript-probe", key: "noscript-probe",
@ -230,21 +80,13 @@ async function init(oldPage = false) {
documentUrl: document.URL, documentUrl: document.URL,
type: window === window.top ? "main_frame" : "script", type: window === window.top ? "main_frame" : "script",
}, },
allowed: canScript allowed: ns.canScript
} });
);
debug(`Loading NoScript in document %s, scripting=%s, readyState %s`, if (!ns.canScript) {
document.URL, canScript, document.readyState); if (document.readyState !== "loading") onScriptDisabled();
window.addEventListener("DOMContentLoaded", onScriptDisabled);
if (/application|video|audio/.test(document.contentType)) {
debug("Embedding document detected");
embeddingDocument = true;
window.addEventListener("pageshow", e => {
debug("Active content still in document %s: %o", document.url, document.querySelectorAll("embed,object,video,audio"));
}, true);
// document.write("<plaintext>");
} }
notifyPage(); notifyPage();
addEventListener("pageshow", notifyPage); });
}

21
src/content/dynamicNS.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
// ensure the order which manifest scripts and dynamically registered scripts
// are executed in doesn't matter for initialization, by using a stub.
if (!this.ns) {
let deferredSetup = null;
let nsStub = this.ns = {
config: {},
setup(DEFAULT, MARKER) {
deferredSetup = [DEFAULT, MARKER];
},
merge: ns => {
ns.config = Object.assign(ns.config, nsStub.config);
this.ns = ns;
if (deferredSetup) {
ns.setup(...deferredSetup);
}
}
}
}

View File

@ -0,0 +1,20 @@
if (ns.embeddingDocument) {
ns.on("capabilities", () => {
for (let policyType of ["object", "media"]) {
if (!ns.allows(policyType)) {
let request = {
id: `noscript-${policyType}-doc`,
type: policyType,
url: document.URL,
documentUrl: document.URL,
embeddingDocument: true,
};
let ph = PlaceHolder.create(policyType, request);
if (ph.replacements.size > 0) {
debug(`Created placeholder for ${policyType} at ${document.URL}`);
seen.record({policyType, request, allowed: false});
}
}
}
});
}

View File

@ -1,5 +1,5 @@
ns.on("perms", event => { ns.on("capabilities", event => {
debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
let mediaBlocker = !ns.allows("media"); let mediaBlocker = !ns.allows("media");
let unpatched = new Map(); let unpatched = new Map();
function patch(obj, methodName, replacement) { function patch(obj, methodName, replacement) {

99
src/content/staticNS.js Normal file
View File

@ -0,0 +1,99 @@
'use strict';
{
let listenersMap = new Map();
let backlog = new Set();
let ns = {
debug: true, // DEV_ONLY
get embeddingDocument() {
delete this.embeddingDocument;
return this.embeddingDocument = CSP.isEmbedType(document.contentType);
},
on(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (!listeners) listenersMap.set(eventName, listeners = new Set());
listeners.add(listener);
if (backlog.has(eventName)) this.fire(eventName, listener);
},
detach(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (listeners) listeners.delete(listener);
},
fire(eventName, listener = null) {
if (listener) {
listener({type:eventName, source: this});
return;
}
let listeners = listenersMap.get(eventName);
if (listeners) {
for (let l of listeners) {
this.fire(eventName, l);
}
}
backlog.add(eventName);
},
setup(DEFAULT, MARKER) {
this.config.DEFAULT = DEFAULT;
if(!this.config.CURRENT) this.config.CURRENT = DEFAULT;
// ugly hack: since now we use registerContentScript instead of the
// filterRequest dynamic script injection hack, we use top.name
// to store per-tab information. We don't want web content to
// mess with it, though, so we wrap it around auto-hiding accessors
this.config.MARKER = MARKER;
let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
if (eraseTabInfoRx.test(top.name)) {
let _name = top.name;
let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
if (top === window) { // wrap to hide
Reflect.defineProperty(top.wrappedJSObject, "name", {
get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
set: exportFunction(value => {
let preamble = top.name.match(tabInfoRx);
top.name = `${preamble && preamble[0] || ""}${value}`;
return value;
}, top.wrappedJSObject)
});
}
let tabInfoMatch = _name.match(tabInfoRx);
if (tabInfoMatch) try {
this.config.tabInfo = JSON.parse(tabInfoMatch[1]);
} catch (e) {
error(e);
}
}
if (!this.config.DEFAULT || this.config.tabInfo.unrestricted) {
this.allows = () => true;
this.capabilities = Object.assign(
new Set(["script"]), { has() { return true; } });
} else {
let perms = this.config.CURRENT;
this.capabilities = new Set(perms.capabilities);
new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
}
this.canScript = this.allows("script");
this.fire("capabilities");
},
config: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
allows(cap) {
return this.capabilities && this.capabilities.has(cap);
},
getWindowName() {
let marker = this.config.MARKER;
return (top === window && marker) ?
window.name.split(`${marker},`).pop()
: window.name;
}
};
if (this.ns) {
this.ns.merge(ns);
} else {
this.ns = ns;
}
}

View File

@ -1,5 +1,5 @@
ns.on("perms", event => { ns.on("capabilities", event => {
debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
if (ns.allows("webgl")) return; if (ns.allows("webgl")) return;
let proto = HTMLCanvasElement.prototype; let proto = HTMLCanvasElement.prototype;
let getContext = proto.getContext; let getContext = proto.getContext;

View File

@ -19,4 +19,5 @@ class CSP {
} }
} }
CSP.isEmbedType = type => /\b(?:application|video|audio)\b/.test(type);
CSP.headerName = "content-security-policy"; CSP.headerName = "content-security-policy";

View File

@ -27,4 +27,6 @@ class NetCSP extends CSP {
return `${this.start}${super.build(...directives)}${this.end}`; return `${this.start}${super.build(...directives)}${this.end}`;
} }
cleanup(headers) {
}
} }

View File

@ -59,6 +59,14 @@
}, },
"content_scripts": [ "content_scripts": [
{
"matches": ["<all_urls>"],
"match_about_blank": true,
"all_frames": true,
"css": [
"/content/content.css"
]
},
{ {
"run_at": "document_start", "run_at": "document_start",
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
@ -71,19 +79,13 @@
"common/CapsCSP.js", "common/CapsCSP.js",
"content/DocumentCSP.js", "content/DocumentCSP.js",
"content/onScriptDisabled.js", "content/onScriptDisabled.js",
"content/staticNS.js",
"content/content.js", "content/content.js",
"content/webglHook.js",
"content/PlaceHolder.js", "content/PlaceHolder.js",
"content/embeddingDocument.js",
"content/webglHook.js",
"content/media.js" "content/media.js"
] ]
},
{
"matches": ["<all_urls>"],
"match_about_blank": true,
"all_frames": true,
"css": [
"/content/content.css"
]
} }
], ],

View File

@ -1,4 +1,4 @@
ns.on("perms", event => { ns.on("capabilities", event => {
if (ns.allows("script")) { if (ns.allows("script")) {
let name = ns.getWindowName(); let name = ns.getWindowName();
if (/[<"'\`(=:]/.test(name)) { if (/[<"'\`(=:]/.test(name)) {