From 57d883d63e9b8232e275dac7eec24f84c040dd24 Mon Sep 17 00:00:00 2001 From: hackademix Date: Wed, 15 Aug 2018 16:41:55 +0200 Subject: Policy serialization using the contentScripts API. --- src/bg/ChildPolicies.js | 111 +++++++++++++++++++++++++++++++++++++++++++++++ src/bg/RequestGuard.js | 17 +------- src/bg/main.js | 5 ++- src/content/content.js | 30 ++++++++++++- src/content/media.js | 103 ++++++++++++++++++++++--------------------- src/content/webglHook.js | 11 ++--- src/manifest.json | 5 ++- 7 files changed, 205 insertions(+), 77 deletions(-) create mode 100644 src/bg/ChildPolicies.js (limited to 'src') diff --git a/src/bg/ChildPolicies.js b/src/bg/ChildPolicies.js new file mode 100644 index 0000000..fd790c9 --- /dev/null +++ b/src/bg/ChildPolicies.js @@ -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");`, + [""]); + } + } +} diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index fcd6fa7..703623f 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -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); } diff --git a/src/bg/main.js b/src/bg/main.js index 47ee89e..2eb2896 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -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() }); diff --git a/src/content/content.js b/src/content/content.js index 886c363..37d0d32 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -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 = { diff --git a/src/content/media.js b/src/content/media.js index ffa67e0..5da239f 100644 --- a/src/content/media.js +++ b/src/content/media.js @@ -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; diff --git a/src/content/webglHook.js b/src/content/webglHook.js index d4c064a..67f31eb 100644 --- a/src/content/webglHook.js +++ b/src/content/webglHook.js @@ -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; diff --git a/src/manifest.json b/src/manifest.json index 61f618b..fd0e027 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -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" ] }, { -- cgit v1.2.3