diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bg/ChildPolicies.js | 3 | ||||
-rw-r--r-- | src/bg/ContentScriptOnce.js | 52 | ||||
-rw-r--r-- | src/bg/RequestUtil.js | 169 | ||||
-rw-r--r-- | src/bg/Settings.js | 2 | ||||
-rw-r--r-- | src/bg/deferWebTraffic.js | 10 | ||||
-rw-r--r-- | src/bg/main.js | 2 | ||||
-rw-r--r-- | src/content/content.js | 27 | ||||
-rw-r--r-- | src/content/media.js | 6 | ||||
-rw-r--r-- | src/content/webglHook.js | 3 | ||||
-rw-r--r-- | src/lib/ResponseMetaData.js | 54 | ||||
-rw-r--r-- | src/manifest.json | 1 | ||||
-rw-r--r-- | src/xss/InjectionChecker.js | 2 | ||||
-rw-r--r-- | src/xss/XSS.js | 5 | ||||
-rw-r--r-- | src/xss/sanitizeName.js | 13 |
14 files changed, 96 insertions, 253 deletions
diff --git a/src/bg/ChildPolicies.js b/src/bg/ChildPolicies.js index 0a77b8b..5b1b209 100644 --- a/src/bg/ChildPolicies.js +++ b/src/bg/ChildPolicies.js @@ -58,8 +58,9 @@ var ChildPolicies = { async storeTabInfo(tabId, info) { try { + let preamble = info ? `${marker} + ${JSON.stringify(JSON.stringify([info]))} + ${marker} + "," + ` : ""; await browser.tabs.executeScript(tabId, { - code: `window.name = ${marker} + ${JSON.stringify(JSON.stringify([info]))} + ${marker} + "," + window.name;`, + code: `window.name = ${preamble}window.name.split(${marker} + ",").pop();`, allFrames: false, matchAboutBlank: true, runAt: "document_start", diff --git a/src/bg/ContentScriptOnce.js b/src/bg/ContentScriptOnce.js new file mode 100644 index 0000000..5be602e --- /dev/null +++ b/src/bg/ContentScriptOnce.js @@ -0,0 +1,52 @@ +var ContentScriptOnce = (() => { + "use strict"; + + let requestMap = new Map(); + + { + let cleanup = r => { + let {requestId} = r; + let scripts = requestMap.get(requestId); + if (scripts) { + window.setTimeout(() => { + requestMap.delete(requestId); + for (let s of scripts) s.unregister(); + }, 0); + } + } + + let filter = { + urls: ["<all_urls>"], + types: ["main_frame", "sub_frame", "object"] + }; + let wr = browser.webRequest; + for (let event of ["onCompleted", "onErrorOccurred"]) { + wr[event].addListener(cleanup, filter); + } + } + + return { + async execute(request, options) { + let {requestId, url} = request; + let scripts = requestMap.get(requestId); + if (!scripts) requestMap.set(requestId, scripts = new Set()); + try { + let urlObj = new URL(url); + if (urlObj.port) { + urlObj.port = ""; + url = urlObj.toString(); + } + } catch (e) {} + let defOpts = { + runAt: "document_start", + matchAboutBlank: true, + matches: [url], + allFrames: true, + }; + + scripts.add(await browser.contentScripts.register( + Object.assign(defOpts, options) + )); + } + } +})(); diff --git a/src/bg/RequestUtil.js b/src/bg/RequestUtil.js deleted file mode 100644 index d6d3300..0000000 --- a/src/bg/RequestUtil.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict'; -{ - let xmlFeedOrImage = /^(?:(?:application|text)\/(?:(?:r(?:ss|df)|atom)\+)xml(;|$))|image\//i; - let rawXml = /^(?:application|text)\/xml;/i; - let brokenXMLOnLoad; - (async () => brokenXMLOnLoad = parseInt((await browser.runtime.getBrowserInfo()).version) < 61)() - - let pendingScripts = new Map(); - let NOP = () => {}; - - let reloadingTabs = new Map(); - let tabKey = (tabId, url) => `${tabId}:${url}`; - - let cleanup = r => { - pendingScripts.delete(r.requestId); - let key = tabKey(r.tabId, r.url); - if (reloadingTabs.get(key) === false) { - reloadingTabs.delete(key); - } - }; - - let executeAll = async request => { - let {url, tabId, frameId, requestId, type} = request; - let scripts = pendingScripts.get(requestId); - if (!scripts) return -1; - pendingScripts.delete(requestId); - let count = 0; - let run = async details => { - details = Object.assign({ - runAt: "document_start", - matchAboutBlank: true, - frameId - }, details); - try { - let res; - for (let attempts = 10; attempts-- > 0;) { - try { - res = await browser.tabs.executeScript(tabId, details); - break; - } catch(e) { - if (!/No matching message handler/.test(e.message)) throw e; - debug("Couldn't inject script into %s: too early? Retrying up to %s times...", url, attempts); - } - } - count++; - debug("Execute on start OK, result=%o", res, url, details); - } catch (e) { - error(e, "Execute on start failed", url, details); - } - }; - await Promise.all([...scripts.values()].map(run)); - return count; - }; - - { - let filter = { - urls: ["<all_urls>"], - types: ["main_frame", "sub_frame"] - }; - let wr = browser.webRequest; - for (let event of ["onCompleted", "onErrorOccurred"]) { - wr[event].addListener(cleanup, filter); - } - - wr.onResponseStarted.addListener(r => { - let scripts = pendingScripts.get(r.requestId); - if (scripts) scripts.runAndFlush(); - }, filter); - } - - var RequestUtil = { - - getResponseMetaData(request) { - return request.response || (request.response = new ResponseMetaData(request)); - }, - - executeOnStart(request, details) { - let {requestId, url, tabId, frameId, statusCode, type} = request; - - if (statusCode >= 300 && statusCode < 400 || type === "object") return; - if (frameId === 0) { - let key = tabKey(tabId, url); - debug("Checking whether %s is a reloading tab...", key); - if (reloadingTabs.get(key)) { - reloadingTabs.set(key, false); // doom it for removal in cleanup - return; - } - } - - let response = this.getResponseMetaData(request); - let {contentType, contentDisposition} = response; - if (contentDisposition || - xmlFeedOrImage.test(contentType) && !/\/svg\b/i.test(contentType)) { - debug("Skipping execute on start of %s %o.", url, response); - return; - } - - debug("Injecting script on start in %s (%o).", url, response); - - let scripts = pendingScripts.get(requestId); - let scriptKey = JSON.stringify(details); - if (!scripts) { - pendingScripts.set(requestId, scripts = new Map()); - scripts.set(scriptKey, details); - } else { - scripts.set(scriptKey, details); - return; - } - - if (/^(?:application|text)\//.test(contentType) - && !/[^;]+\b(html|xml)\b/i.test(contentType)) { - debug("Not HTML: defer script to onResponseStarted for %s (%o)", url, response); - return; - } - - let mustCheckFeed = brokenXMLOnLoad && frameId === 0 && rawXml.test(contentType); - debug("mustCheckFeed = %s, brokenXMLOnLoad = %s", mustCheckFeed, brokenXMLOnLoad); - let filter = browser.webRequest.filterResponseData(requestId); - let buffer = []; - let responseCompleted = false; - let mustReload = false; - scripts.runAndFlush = async () => { - scripts.runAndFlush = NOP; - if (responseCompleted && buffer && !buffer.length) { - filter.disconnect(); - } - let scriptsRan = await executeAll(request); - if (mustCheckFeed && !scriptsRan) { - mustReload = true; - debug(`Marking as "must reload"`, tabId, url); - reloadingTabs.set(tabKey(tabId, url), true); - } - if (buffer) { - debug("Flushing %s buffer chunks on %s", buffer.length, url); - for (let chunk of buffer) { - filter.write(chunk); - } - buffer = null; - } - filter.disconnect(); - if (responseCompleted) { - filter.onstop(null); - } - }; - - filter.ondata = event => { - scripts.runAndFlush(); - if (buffer) { - debug("buffering", url); - buffer.push(event.data); - return; - } - - debug("ondata", url); - filter.write(event.data); - filter.disconnect(); - }; - - filter.onstop = async event => { - responseCompleted = true; - await scripts.runAndFlush(); - if (mustReload && !buffer) { - mustReload = false; - browser.tabs.update(tabId, {url}); - } - } - } - } -} diff --git a/src/bg/Settings.js b/src/bg/Settings.js index 09f0edd..56c88f4 100644 --- a/src/bg/Settings.js +++ b/src/bg/Settings.js @@ -90,7 +90,7 @@ var Settings = { if (typeof unrestrictedTab === "boolean") { ns.unrestrictedTabs[unrestrictedTab ? "add" : "delete"](tabId); - ChildPolicies.storeTabInfo(tabId, {unrestricted: unrestrictedTab}); + ChildPolicies.storeTabInfo(tabId, unrestrictedTab && {unrestricted: true}); } if (reloadAffected) { browser.tabs.reload(tabId); diff --git a/src/bg/deferWebTraffic.js b/src/bg/deferWebTraffic.js index fced870..15d177b 100644 --- a/src/bg/deferWebTraffic.js +++ b/src/bg/deferWebTraffic.js @@ -34,16 +34,20 @@ function deferWebTraffic(promiseToWaitFor, next) { if (frameId !== 0) { documentUrl = request.frameAncestors.pop().url; } - reloadTab(tabId); + if (tabId !== -1) { + reloadTab(tabId); + } else { + debug("No tab to reload for %s %s from %s", type, url, documentUrl); + } } } - debug("Deferring ", url, type); + debug("Deferring %s %s from %s", type, url, documentUrl); try { await promiseToWaitFor; } catch (e) { error(e); } - debug("Green light to ", url, type); + debug("Green light to %s %s from %s", type, url, documentUrl); } function spyTabs(request) { diff --git a/src/bg/main.js b/src/bg/main.js index 2eb2896..75ea868 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -36,7 +36,7 @@ await include("/bg/defaults.js"); await ns.defaults; - await include(["/bg/RequestGuard.js", "/bg/RequestUtil.js"]); + await include("/bg/RequestGuard.js"); await RequestGuard.start(); await XSS.start(); // we must start it anyway to initialize sub-objects if (!ns.sync.xss) { diff --git a/src/content/content.js b/src/content/content.js index 2f10d5a..8f772b1 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -3,23 +3,30 @@ // debug = () => {}; // REL_ONLY { 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) { + fire(eventName, listener = null) { + if (listener) { + listener({type:eventName, source: this}); + return; + } let listeners = listenersMap.get(eventName); if (listeners) { for (let l of listeners) { - l(this); + this.fire(eventName, l); } } + backlog.add(eventName); }, setup(DEFAULT, MARKER) { this.perms.DEFAULT = DEFAULT; @@ -36,10 +43,10 @@ let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`); if (top === window) { // wrap to hide Reflect.defineProperty(top.wrappedJSObject, "name", { - get: exportFunction(() => _name.replace(eraseTabInfoRx, ""), top.wrappedJSObject), + get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject), set: exportFunction(value => { - let preamble = _name.match(tabInfoRx); - _name = `${preamble && preamble[0] || ""}${value}`; + let preamble = top.name.match(tabInfoRx); + top.name = `${preamble && preamble[0] || ""}${value}`; return value; }, top.wrappedJSObject) }); @@ -57,14 +64,14 @@ } ns.fire("perms"); }, - storeTabInfo(info) { - let {MARKER} = this.perms; - window.name = `${MARKER}${JSON.stringify([info])}${MARKER},${window.name.split(marker).pop()}`; - }, - perms: { DEFAULT: null, CURRENT: null, tabInfo: {} }, + perms: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" }, allows(cap) { let perms = this.perms.CURRENT; return perms && perms.capabilities.includes(cap); + }, + getWindowName() { + return top !== window || !this.perms.MARKER ? window.name + : window.name.split(this.perms.MARKER + ",").pop(); } } } diff --git a/src/content/media.js b/src/content/media.js index 5da239f..910fd27 100644 --- a/src/content/media.js +++ b/src/content/media.js @@ -1,4 +1,4 @@ -ns.on("perms", ns => { +ns.on("perms", event => { debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY let mediaBlocker = !ns.allows("media"); let unpatched = new Map(); @@ -54,6 +54,4 @@ ns.on("perms", ns => { return unpatched.get(window.MediaSource.prototype).addSourceBuffer.call(ms, mime, ...args); }); - -})(); -document.URL; +}); diff --git a/src/content/webglHook.js b/src/content/webglHook.js index 67f31eb..efafcd5 100644 --- a/src/content/webglHook.js +++ b/src/content/webglHook.js @@ -1,4 +1,4 @@ -ns.on("perms", ns => { +ns.on("perms", event => { debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY if (ns.allows("webgl")) return; let proto = HTMLCanvasElement.prototype; @@ -26,4 +26,3 @@ ns.on("perms", ns => { return getContext.call(this, type, ...rest); }, proto, {defineAs: "getContext"}); }); -document.URL; diff --git a/src/lib/ResponseMetaData.js b/src/lib/ResponseMetaData.js deleted file mode 100644 index 0d86745..0000000 --- a/src/lib/ResponseMetaData.js +++ /dev/null @@ -1,54 +0,0 @@ -class ResponseMetaData { - constructor(request) { - let {responseHeaders} = request; - this.headers = {}; - this.contentType = this.contentDisposition = null; - for (let h of responseHeaders) { - if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) { - let propertyName = RegExp.$1; - propertyName = `content${propertyName.charAt(0).toUpperCase()}${propertyName.substring(1).toLowerCase()}`; - this[propertyName] = h.value; - this.headers[propertyName] = h; - } - } - this.forcedUTF8 = false; - } - - get charset() { - let charset = ""; - if (this.contentType) { - let m = this.contentType.match(/;\s*charset\s*=\s*(\S+)/); - if (m) { - charset = m[1]; - } - } - Object.defineProperty(this, "charset", { value: charset, writable: false, configurable: true }); - return charset; - } - - get isUTF8() { - return /^utf-?8$/i.test(this.charset); - } - - forceUTF8() { - if (!(this.forcedUTF8 || this.isUTF8)) { - let h = this.headers.contentType; - if (h) { - h.value = h.value.replace(/;\s*charset\s*=.*|$/, "; charset=utf8"); - this.forcedUTF8 = true; - } // if the header doesn't exist the browser should default to UTF-8 anyway - } - return this.forcedUTF8; - } - - createDecoder() { - if (this.charset) { - try { - return new TextDecoder(this.charset); - } catch (e) { - console.error(e); - } - } - return new TextDecoder("utf-8"); - } -}; diff --git a/src/manifest.json b/src/manifest.json index fd0e027..024fb73 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -39,7 +39,6 @@ "lib/include.js", "lib/punycode.js", "lib/tld.js", - "lib/ResponseMetaData.js", "lib/LastListener.js", "common/Policy.js", "common/locale.js", diff --git a/src/xss/InjectionChecker.js b/src/xss/InjectionChecker.js index 8e2598e..93d32d9 100644 --- a/src/xss/InjectionChecker.js +++ b/src/xss/InjectionChecker.js @@ -741,7 +741,7 @@ XSS.InjectionChecker = (async () => { checkJS: function(s, unescapedUni) { this.log(s); - if (/\?name\b[\s\S]*:|[^&?]\bname\b/.test(s)) { + if (/[=\(](?:[\s\S]*(?:\?name\b[\s\S]*:|[^&?]\bname\b)|name\b)/.test(s)) { this.nameAssignment = true; } diff --git a/src/xss/XSS.js b/src/xss/XSS.js index 94e33fa..89f13f7 100644 --- a/src/xss/XSS.js +++ b/src/xss/XSS.js @@ -61,8 +61,9 @@ var XSS = (() => { let block = !!(reasons.urlInjection || reasons.postInjection) if (reasons.protectName) { - RequestUtil.executeOnStart(request, { - file: "/xss/sanitizeName.js", + await include("bg/COntentScriptOnce.js"); + await ContentScriptOnce.execute(request, { + js: [{file: "/xss/sanitizeName.js"}], }); if (!block) return ALLOW; } diff --git a/src/xss/sanitizeName.js b/src/xss/sanitizeName.js index 22185f4..4f36cbf 100644 --- a/src/xss/sanitizeName.js +++ b/src/xss/sanitizeName.js @@ -1,4 +1,9 @@ -if (/[<"'\`(=:]/.test(window.name)) { - console.log(`NoScript XSS filter sanitizing suspicious window.name "%s" on %s`, window.name, document.URL); - window.name = ""; -} +ns.on("perms", event => { + if (ns.allows("script")) { + let name = ns.getWindowName(); + if (/[<"'\`(=:]/.test(name)) { + console.log(`NoScript XSS filter sanitizing suspicious window.name "%s" on %s`, name, document.URL); + window.name = window.name.substring(0, window.name.length - name.length); + } + } +}); |