Refactored XSS filter into an asynchronous worker to better handle DOS attempts.
This commit is contained in:
parent
e48c2053df
commit
9a664f7b3b
|
@ -15,7 +15,7 @@ class Timing {
|
|||
}
|
||||
|
||||
async pause() {
|
||||
if (this.interrupted) throw new TimingException("Interrupted");
|
||||
if (this.interrupted) throw new TimingException("Timing: interrupted");
|
||||
let now = Date.now();
|
||||
this.calls++;
|
||||
let sinceLastCall = now - this.lastCall;
|
||||
|
@ -28,7 +28,7 @@ class Timing {
|
|||
if (now - this.lastPause > this.workSlot || this.calls > this.maxCalls) {
|
||||
this.tooLong = this.elapsed >= this.longTime;
|
||||
if (this.tooLong && this.fatalTimeout) {
|
||||
throw new TimingException(`Exceeded ${this.longTime}ms timeout`);
|
||||
throw new TimingException(`Timing: exceeded ${this.longTime}ms timeout`);
|
||||
}
|
||||
this.calls = 0;
|
||||
await Timing.sleep(this.pauseTime);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
{
|
||||
let PREFIX = `[${browser.runtime.getManifest().name}]`;
|
||||
let PREFIX = typeof browser === "object"
|
||||
? `[${browser.runtime.getManifest().name}]` : '';
|
||||
|
||||
let debugCount = 0;
|
||||
|
||||
function log(msg, ...rest) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
if (UA.isMozilla) {
|
||||
let y = async (url, originUrl = '') => await XSS.maybe({originUrl, url, method: "GET"});
|
||||
let y = async (url, originUrl = '') => await XSS.maybe(XSS.parseRequest({originUrl, url, method: "GET"}));
|
||||
let n = async (...args) => !await y(...args);
|
||||
Promise.all([
|
||||
() => y("https://noscript.net/<script"),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
118
src/xss/XSS.js
118
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 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;
|
||||
}
|
||||
|
||||
let protectName = ic.nameAssignment;
|
||||
let urlInjection = await ic.checkUrl(destUrl, skipRx);
|
||||
protectName = protectName || ic.nameAssignment;
|
||||
|
||||
return !(protectName || postInjection || urlInjection) ? null
|
||||
: { protectName, postInjection, urlInjection };
|
||||
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)
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
Loading…
Reference in New Issue