summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bg/ChildPolicies.js3
-rw-r--r--src/bg/ContentScriptOnce.js52
-rw-r--r--src/bg/RequestUtil.js169
-rw-r--r--src/bg/Settings.js2
-rw-r--r--src/bg/deferWebTraffic.js10
-rw-r--r--src/bg/main.js2
-rw-r--r--src/content/content.js27
-rw-r--r--src/content/media.js6
-rw-r--r--src/content/webglHook.js3
-rw-r--r--src/lib/ResponseMetaData.js54
-rw-r--r--src/manifest.json1
-rw-r--r--src/xss/InjectionChecker.js2
-rw-r--r--src/xss/XSS.js5
-rw-r--r--src/xss/sanitizeName.js13
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);
+ }
+ }
+});