Policy serialization using the contentScripts API.

This commit is contained in:
hackademix 2018-08-15 16:41:55 +02:00
parent 2c75eedadd
commit 57d883d63e
7 changed files with 205 additions and 77 deletions

111
src/bg/ChildPolicies.js Normal file
View File

@ -0,0 +1,111 @@
"use script";
{
let Scripts = {
references: new Set(),
opts: {
js: [{}],
allFrames: true,
matchAboutBlank: true,
runAt: "document_start"
},
forget() {
for (let script of [...this.references]) {
script.unregister();
this.references.delete(script);
}
},
async register(code, matches, excludeMatches) {
debug("Registering child policy.", code, matches, excludeMatches);
if (!matches.length) return;
try {
this.opts.js[0].code = code;
this.opts.matches = matches;
if (excludeMatches && excludeMatches.length) {
this.opts.excludeMatches = excludeMatches;
} else {
delete this.opts.excludeMatches;
}
this.references.add(await browser.contentScripts.register(this.opts));
} catch (e) {
error(e);
}
}
};
let flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
let protocolRx = /^(https?):/i;
let pathRx = /[^:/]\//;
let portRx = /:\d+(?=\/|$)/;
let validMatchPatternRx = /^(?:https?|\*):\/\/(?:\*\.)?(?:[\w\u0100-\uf000][\w\u0100-\uf000.-]*)?[\w\u0100-\uf000]\/(\*|[^*]*)$/;
let siteKey2MatchPattern = site => {
let hasProtocol = site.match(protocolRx);
let protocol = hasProtocol ? ''
: Sites.isSecureDomainKey(site) ? "https://" : "*://";
let hostname = Sites.toggleSecureDomainKey(site, false)
.replace(portRx, '');
if (!hasProtocol) hostname = `*.${hostname}`;
let path = pathRx.test(hostname) ? "" : "/*";
let mp = `${protocol}${hostname}${path}`;
return validMatchPatternRx.test(mp) && (path ? mp : [mp, `${mp}?*`, `${mp}#*`]);
};
let siteKeys2MatchPatterns = keys => keys && flatten(keys.map(siteKey2MatchPattern)).filter(p => !!p) || [];
var ChildPolicies = {
async update(policy) {
let serialized = policy.dry ? policy.dry(true) : policy;
let permsMap = new Map();
let trusted = JSON.stringify(serialized.TRUSTED);
let untrusted = JSON.stringify(serialized.UNTRUSTED);
let presets = {
trusted,
untrusted,
temp: trusted
};
// map presets to site keys
for (let [container, perms] of Object.entries(presets)) {
let newKeys = serialized.sites[container];
if (!(newKeys && newKeys.length)) continue;
let keys = permsMap.get(perms);
if (keys) {
newKeys = keys.concat(newKeys);
}
permsMap.set(perms, newKeys);
}
// map custom permissions to site keys
for (let [key, perms] of Object.entries(serialized.sites.custom)) {
let permsKey = JSON.stringify(perms);
let keys = permsMap.get(permsKey);
if (keys) {
keys.push(key);
} else {
permsMap.set(permsKey, [key]);
}
}
// compute exclusions
let permsMapEntries = [...permsMap];
let excludeMap = new Map();
for (let [perms, keys] of permsMapEntries) {
excludeMap.set(perms, siteKeys2MatchPatterns(flatten(
permsMapEntries.filter(([other]) => other !== perms)
.map(([otherPerms, otherKeys]) => otherKeys))
.filter(k => k && k.includes("/"))
));
}
Scripts.forget();
// register new content scripts
for (let [perms, keys] of [...permsMap]) {
await Scripts.register(`ns.perms.CURRENT = ${perms};`, siteKeys2MatchPatterns(keys), excludeMap.get(perms));
}
await Scripts.register(
`ns.perms.DEFAULT = ${JSON.stringify(serialized.DEFAULT)};
if(!ns.perms.CURRENT) ns.perms.CURRENT = ns.perms.DEFAULT;
ns.fire("perms");`,
["<all_urls>"]);
}
}
}

View File

@ -437,22 +437,7 @@ var RequestGuard = (() => {
blocker = CSP.createBlocker(...blockedTypes);
}
if (canScript && !isObject) {
if (!capabilities.has("webgl")) {
RequestUtil.executeOnStart(request, {
file: "/content/webglHook.js"
});
}
if (!capabilities.has("media")) {
RequestUtil.executeOnStart(request, {
code: "window.mediaBlocker = true;"
});
}
RequestUtil.executeOnStart(request, {
file: "/content/media.js"
});
} else if (request.type === "main_frame" && !TabStatus.map.has(tabId)) {
if (request.type === "main_frame" && !TabStatus.map.has(tabId)) {
debug("No TabStatus data yet for noscriptFrame", tabId);
TabStatus.record(request, "noscriptFrame", true);
}

View File

@ -26,12 +26,14 @@
let policyData = (await Storage.get("sync", "policy")).policy;
if (policyData && policyData.DEFAULT) {
ns.policy = new Policy(policyData);
await ChildPolicies.update(policyData);
} else {
await include("/legacy/Legacy.js");
ns.policy = await Legacy.createOrMigratePolicy();
ns.savePolicy();
}
await include("/bg/defaults.js");
await ns.defaults;
await include(["/bg/RequestGuard.js", "/bg/RequestUtil.js"]);
@ -226,6 +228,7 @@
async savePolicy() {
if (this.policy) {
await ChildPolicies.update(this.policy);
await Storage.set("sync", {
policy: this.policy.dry()
});

View File

@ -1,7 +1,34 @@
'use strict';
// debug = () => {}; // REL_ONLY
{
let listenersMap = new Map();
var ns = {
on(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (!listeners) listenersMap.set(eventName, listeners = new Set());
listeners.add(listener);
},
detach(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (listeners) listeners.delete(listener);
},
fire(eventName) {
let listeners = listenersMap.get(eventName);
if (listeners) {
for (let l of listeners) {
l(this);
}
}
},
perms: { DEFAULT: null, CURRENT: null },
allows(cap) {
let perms = this.perms.CURRENT;
return perms && perms.capabilities.includes(cap);
}
}
}
var canScript = true, shouldScript = false;
let now = () => performance.now() + performance.timeOrigin;
@ -24,7 +51,6 @@ function probe() {
var _ = browser.i18n.getMessage;
var embeddingDocument = false;
var seen = {

View File

@ -1,60 +1,59 @@
{
debug("Media Hook (blocked %s)", !!window.mediaBlocker, document.URL, document.documentElement && document.documentElement.innerHTML); // DEV_ONLY
(() => {
let unpatched = new Map();
function patch(obj, methodName, replacement) {
let methods = unpatched.get(obj) || {};
methods[methodName] = obj[methodName];
exportFunction(replacement, obj, {defineAs: methodName});
unpatched.set(obj, methods);
ns.on("perms", ns => {
debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
let mediaBlocker = !ns.allows("media");
let unpatched = new Map();
function patch(obj, methodName, replacement) {
let methods = unpatched.get(obj) || {};
methods[methodName] = obj[methodName];
exportFunction(replacement, obj, {defineAs: methodName});
unpatched.set(obj, methods);
}
let urlMap = new WeakMap();
patch(window.URL, "createObjectURL", function(o, ...args) {
let url = unpatched.get(window.URL).createObjectURL.call(this, o, ...args);
if (o instanceof MediaSource) {
let urls = urlMap.get(o);
if (!urls) urlMap.set(o, urls = new Set());
urls.add(url);
}
let urlMap = new WeakMap();
patch(window.URL, "createObjectURL", function(o, ...args) {
let url = unpatched.get(window.URL).createObjectURL.call(this, o, ...args);
if (o instanceof MediaSource) {
let urls = urlMap.get(o);
if (!urls) urlMap.set(o, urls = new Set());
urls.add(url);
}
return url;
});
return url;
});
patch(window.MediaSource.prototype, "addSourceBuffer", function(mime, ...args) {
let ms = this;
let urls = urlMap.get(ms);
patch(window.MediaSource.prototype, "addSourceBuffer", function(mime, ...args) {
let ms = this;
let urls = urlMap.get(ms);
let request = {
id: "noscript-media",
type: "media",
url: document.URL,
documentUrl: document.URL,
embeddingDocument: true,
};
seen.record({policyType: "media", request, allowed: false});
notifyPage();
let request = {
id: "noscript-media",
type: "media",
url: document.URL,
documentUrl: document.URL,
embeddingDocument: true,
};
seen.record({policyType: "media", request, allowed: false});
notifyPage();
if (window.mediaBlocker) {
(async () => {
let me = Array.from(document.querySelectorAll("video,audio"))
.find(e => e.srcObject === ms || urls && urls.has(e.src));
if (mediaBlocker) {
(async () => {
let me = Array.from(document.querySelectorAll("video,audio"))
.find(e => e.srcObject === ms || urls && urls.has(e.src));
if (!me) return;
let exposedMime = `${mime} (MSE)`;
if (!me) return;
let exposedMime = `${mime} (MSE)`;
try {
let ph = PlaceHolder.create("media", request);
ph.replace(me);
PlaceHolder.listen();
} catch (e) {
error(e);
}
})();
throw new Error(`${exposedMime} blocked by NoScript`);
}
try {
let ph = PlaceHolder.create("media", request);
ph.replace(me);
PlaceHolder.listen();
} catch (e) {
error(e);
}
})();
throw new Error(`${exposedMime} blocked by NoScript`);
}
return unpatched.get(window.MediaSource.prototype).addSourceBuffer.call(ms, mime, ...args);
});
return unpatched.get(window.MediaSource.prototype).addSourceBuffer.call(ms, mime, ...args);
});
})();
document.URL;
}
})();
document.URL;

View File

@ -1,5 +1,6 @@
{
debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML); // DEV_ONLY
ns.on("perms", ns => {
debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
if (ns.allows("webgl")) return;
let proto = HTMLCanvasElement.prototype;
let getContext = proto.getContext;
exportFunction(function(type, ...rest) {
@ -23,6 +24,6 @@
return {};
}
return getContext.call(this, type, ...rest);
}, proto, {defineAs: "getContext"});
document.URL;
}
}, proto, {defineAs: "getContext"});
});
document.URL;

View File

@ -49,6 +49,7 @@
"ui/Prompts.js",
"xss/XSS.js",
"bg/deferWebTraffic.js",
"bg/ChildPolicies.js",
"bg/main.js"
]
},
@ -63,7 +64,9 @@
"lib/log.js",
"content/onScriptDisabled.js",
"content/content.js",
"content/PlaceHolder.js"
"content/webglHook.js",
"content/PlaceHolder.js",
"content/media.js"
]
},
{