summaryrefslogtreecommitdiff
path: root/src/xss
diff options
context:
space:
mode:
authorhackademix2020-02-29 19:01:45 +0100
committerhackademix2020-02-29 19:01:45 +0100
commit9a664f7b3b6a5315983317b0e47128b809bc5531 (patch)
tree057ff4d7b4cd7c00634c2655c44774e6a9c8a01c /src/xss
parente48c2053dfef4fb9209e3d432738b8fef6b8d507 (diff)
downloadnoscript-9a664f7b3b6a5315983317b0e47128b809bc5531.tar.gz
noscript-9a664f7b3b6a5315983317b0e47128b809bc5531.tar.xz
noscript-9a664f7b3b6a5315983317b0e47128b809bc5531.zip
Refactored XSS filter into an asynchronous worker to better handle DOS attempts.
Diffstat (limited to 'src/xss')
-rw-r--r--src/xss/Exceptions.js4
-rw-r--r--src/xss/InjectionCheckWorker.js81
-rw-r--r--src/xss/InjectionChecker.js6
-rw-r--r--src/xss/XSS.js120
4 files changed, 152 insertions, 59 deletions
diff --git a/src/xss/Exceptions.js b/src/xss/Exceptions.js
index 24fc480..e8db6e1 100644
--- a/src/xss/Exceptions.js
+++ b/src/xss/Exceptions.js
@@ -52,14 +52,14 @@ XSS.Exceptions = (() => {
// destination or @source matching legacy regexp
if (this.legacyExceptions &&
(this.legacyExceptions.test(unescapedDest) &&
- !this.isBadException(destObj.hostname) ||
+ !this.isBadException(xssReq.destDomain) ||
this.legacyExceptions.test("@" + unescape(srcUrl))
)) {
logEx("Legacy exception", this.legacyExceptions);
return true;
}
- if (!srcObj && isGet) {
+ if (!srcOrigin && isGet) {
if (/^https?:\/\/msdn\.microsoft\.com\/query\/[^<]+$/.test(unescapedDest)) {
return true; // MSDN from Microsoft VS
}
diff --git a/src/xss/InjectionCheckWorker.js b/src/xss/InjectionCheckWorker.js
new file mode 100644
index 0000000..47f007d
--- /dev/null
+++ b/src/xss/InjectionCheckWorker.js
@@ -0,0 +1,81 @@
+let include = src => {
+ if (Array.isArray(src)) importScripts(...src);
+ else importScripts(src);
+}
+
+let XSS = {};
+include("/lib/log.js");
+
+for (let logType of ["log", "debug", "error"]) {
+ this[logType] = (...log) => {
+ postMessage({log, logType});
+ }
+}
+
+include("InjectionChecker.js");
+Entities = {
+ convertAll(s) { return s },
+};
+
+{
+ let timingsMap = new Map();
+
+ let Handlers = {
+ async check({xssReq, skip}) {
+ let {destUrl, unparsedRequest: request, debugging} = xssReq;
+ let {
+ skipParams,
+ skipRx
+ } = skip;
+ let ic = new (await XSS.InjectionChecker)();
+
+ if (debugging) {
+ ic.logEnabled = true;
+ debug("[XSS] InjectionCheckWorker started in %s ms (%s).",
+ Date.now() - xssReq.timestamp, destUrl);
+ } else {
+ debug = () => {};
+ }
+
+ let {timing} = ic;
+ timingsMap.set(request.requestId, timing);
+ timing.fatalTimeout = true;
+
+ let postInjection = xssReq.isPost &&
+ request.requestBody && request.requestBody.formData &&
+ await ic.checkPost(request.requestBody.formData, skipParams);
+
+ let protectName = ic.nameAssignment;
+ let urlInjection = await ic.checkUrl(destUrl, skipRx);
+ protectName = protectName || ic.nameAssignment;
+ if (timing.tooLong) {
+ log("[XSS] Long check (%s ms) - %s", timing.elapsed, JSON.stringify(xssReq));
+ } else if (debugging) {
+ debug("[XSS] InjectionCheckWorker done in %s ms (%s).",
+ Date.now() - xssReq.timestamp, destUrl);
+ }
+
+ postMessage(!(protectName || postInjection || urlInjection) ? null
+ : { protectName, postInjection, urlInjection }
+ );
+ },
+
+ requestDone({requestId}) {
+ let timing = timingsMap.get(requestId);
+ if (timing) {
+ timing.interrupted = true;
+ timingsMap.delete(requestId);
+ }
+ }
+ }
+
+ onmessage = async e => {
+ let msg = e.data;
+ if (msg.handler in Handlers) try {
+ await Handlers[msg.handler](msg);
+ } catch (e) {
+ postMessage({error: e});
+ }
+ }
+
+}
diff --git a/src/xss/InjectionChecker.js b/src/xss/InjectionChecker.js
index 157147e..a309891 100644
--- a/src/xss/InjectionChecker.js
+++ b/src/xss/InjectionChecker.js
@@ -1,6 +1,6 @@
-debug("Initializing InjectionChecker");
XSS.InjectionChecker = (async () => {
await include([
+ "/common/SyntaxChecker.js",
"/lib/Base64.js",
"/lib/Timing.js",
"/xss/FlashIdiocy.js",
@@ -1031,7 +1031,7 @@ XSS.InjectionChecker = (async () => {
return true;
if (s.indexOf("&") !== -1) {
- let unent = Entities.convertAll(s);
+ let unent = await Entities.convertAll(s);
if (unent !== s && await this._checkRecursive(unent, depth)) return true;
}
@@ -1050,7 +1050,7 @@ XSS.InjectionChecker = (async () => {
return true;
if (/[\u0000-\u001f]|&#/.test(unescaped)) {
- let unent = Entities.convertAll(unescaped.replace(/[\u0000-\u001f]+/g, ''));
+ let unent = await Entities.convertAll(unescaped.replace(/[\u0000-\u001f]+/g, ''));
if (unescaped != unent && await this._checkRecursive(unent, depth)) {
this.log("Trash-stripped nested URL match!");
return true;
diff --git a/src/xss/XSS.js b/src/xss/XSS.js
index aba48ca..0a21603 100644
--- a/src/xss/XSS.js
+++ b/src/xss/XSS.js
@@ -4,8 +4,8 @@ var XSS = (() => {
const ABORT = {cancel: true}, ALLOW = {};
+ let workersMap = new Map();
let promptsMap = new Map();
- let timingsMap = new Map();
async function getUserResponse(xssReq) {
let {originKey} = xssReq;
@@ -23,10 +23,11 @@ var XSS = (() => {
}
function doneListener(request) {
- let timing = timingsMap.get(request.id);
- if (timing) {
- timing.interrupted = true;
- timingsMap.delete(request.id);
+ let {requestId} = request;
+ let worker = workersMap.get(requestId);
+ if (worker) {
+ worker.terminate();
+ workersMap.delete(requestId);
}
}
@@ -58,7 +59,7 @@ var XSS = (() => {
data = [];
} catch (e) {
error(e, "XSS filter processing %o", xssReq);
- if (e instanceof TimingException) {
+ if (/^Timing:/.test(e.message) && !/\btimeout\b/i.test(e.message)) {
// we don't want prompts if the request expired / errored first
return ABORT;
}
@@ -125,6 +126,21 @@ var XSS = (() => {
}
};
+ function parseUrl(url) {
+ let u = new URL(url);
+ // make it cloneable
+ return {
+ href: u.href,
+ protocol: u.protocol,
+ hostname: u.hostname,
+ port: u.port,
+ origin: u.origin,
+ pathname: u.pathname,
+ search: u.search,
+ hash: u.hash,
+ };
+ }
+
return {
async start() {
if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only
@@ -166,7 +182,6 @@ var XSS = (() => {
}
},
-
parseRequest(request) {
let {
url: destUrl,
@@ -175,7 +190,7 @@ var XSS = (() => {
} = request;
let destObj;
try {
- destObj = new URL(destUrl);
+ destObj = parseUrl(destUrl);
} catch (e) {
error(e, "Cannot create URL object for %s", destUrl);
return null;
@@ -183,7 +198,7 @@ var XSS = (() => {
let srcObj = null;
if (srcUrl) {
try {
- srcObj = new URL(srcUrl);
+ srcObj = parseUrl(srcUrl);
} catch (e) {}
} else {
srcUrl = "";
@@ -198,28 +213,21 @@ var XSS = (() => {
let isGet = method === "GET";
return {
- xssUnparsed: request,
+ unparsedRequest: request,
srcUrl,
destUrl,
srcObj,
destObj,
srcOrigin,
destOrigin,
- get srcDomain() {
- delete this.srcDomain;
- return this.srcDomain = srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "";
- },
- get destDomain() {
- delete this.destDomain;
- return this.destDomain = tld.getDomain(destObj.hostname);
- },
- get originKey() {
- delete this.originKey;
- return this.originKey = `${srcOrigin}>${destOrigin}`;
- },
+ srcDomain: srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "",
+ destDomain: tld.getDomain(destObj.hostname),
+ originKey: `${srcOrigin}>${destOrigin}`,
unescapedDest,
isGet,
isPost: !isGet && method === "POST",
+ timestamp: Date.now(),
+ debugging: ns.local.debug,
}
},
@@ -237,42 +245,46 @@ var XSS = (() => {
return this._userChoices[originKey];
},
- async maybe(request) { // return reason or null if everything seems fine
- let xssReq = request.xssUnparsed ? request : this.parseRequest(request);
- request = xssReq.xssUnparsed;
-
+ async maybe(xssReq) { // return reason or null if everything seems fine
if (await this.Exceptions.shouldIgnore(xssReq)) {
return null;
}
- let {
- skipParams,
- skipRx
- } = this.Exceptions.partial(xssReq);
-
- let {destUrl} = xssReq;
-
- await include("/xss/InjectionChecker.js");
- let ic = new (await this.InjectionChecker)();
- ic.logEnabled = ns.local.debug;
- let {timing} = ic;
- timingsMap.set(request.id, timing);
- timing.fatalTimeout = true;
-
- let postInjection = xssReq.isPost &&
- request.requestBody && request.requestBody.formData &&
- await ic.checkPost(request.requestBody.formData, skipParams);
-
- if (timing.tooLong) {
- log("[XSS] Long check (%s ms) - %s", timing.elapsed, JSON.stringify(xssReq));
- }
-
- let protectName = ic.nameAssignment;
- let urlInjection = await ic.checkUrl(destUrl, skipRx);
- protectName = protectName || ic.nameAssignment;
-
- return !(protectName || postInjection || urlInjection) ? null
- : { protectName, postInjection, urlInjection };
+ let skip = this.Exceptions.partial(xssReq);
+ let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js"));
+ let {requestId} = xssReq.unparsedRequest;
+ workersMap.set(requestId, worker)
+ return await new Promise((resolve, reject) => {
+ let cleanup = () => {
+ workersMap.delete(requestId);
+ worker.terminate();
+ };
+ worker.onmessage = e => {
+ let {data} = e;
+ if (data) {
+ if (data.logType) {
+ window[data.logType](...data.log);
+ return;
+ }
+ if (data.error) {
+ reject(data.error);
+ cleanup();
+ return;
+ }
+ }
+ resolve(e.data);
+ cleanup();
+ }
+ worker.onerror = worker.onmessageerror = e => {
+ reject(e);
+ cleanup();
+ }
+ worker.postMessage({handler: "check", xssReq, skip});
+ setTimeout(() => {
+ reject(new Error("Timeout! DOS attack attempt?"));
+ cleanup();
+ }, 20000)
+ });
}
};
})();