From 9a664f7b3b6a5315983317b0e47128b809bc5531 Mon Sep 17 00:00:00 2001 From: hackademix Date: Sat, 29 Feb 2020 19:01:45 +0100 Subject: Refactored XSS filter into an asynchronous worker to better handle DOS attempts. --- src/xss/XSS.js | 120 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 66 insertions(+), 54 deletions(-) (limited to 'src/xss/XSS.js') 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) + }); } }; })(); -- cgit v1.2.3