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() {
|
async pause() {
|
||||||
if (this.interrupted) throw new TimingException("Interrupted");
|
if (this.interrupted) throw new TimingException("Timing: interrupted");
|
||||||
let now = Date.now();
|
let now = Date.now();
|
||||||
this.calls++;
|
this.calls++;
|
||||||
let sinceLastCall = now - this.lastCall;
|
let sinceLastCall = now - this.lastCall;
|
||||||
|
@ -28,7 +28,7 @@ class Timing {
|
||||||
if (now - this.lastPause > this.workSlot || this.calls > this.maxCalls) {
|
if (now - this.lastPause > this.workSlot || this.calls > this.maxCalls) {
|
||||||
this.tooLong = this.elapsed >= this.longTime;
|
this.tooLong = this.elapsed >= this.longTime;
|
||||||
if (this.tooLong && this.fatalTimeout) {
|
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;
|
this.calls = 0;
|
||||||
await Timing.sleep(this.pauseTime);
|
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;
|
let debugCount = 0;
|
||||||
|
|
||||||
function log(msg, ...rest) {
|
function log(msg, ...rest) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
if (UA.isMozilla) {
|
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);
|
let n = async (...args) => !await y(...args);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
() => y("https://noscript.net/<script"),
|
() => y("https://noscript.net/<script"),
|
||||||
|
|
|
@ -52,14 +52,14 @@ XSS.Exceptions = (() => {
|
||||||
// destination or @source matching legacy regexp
|
// destination or @source matching legacy regexp
|
||||||
if (this.legacyExceptions &&
|
if (this.legacyExceptions &&
|
||||||
(this.legacyExceptions.test(unescapedDest) &&
|
(this.legacyExceptions.test(unescapedDest) &&
|
||||||
!this.isBadException(destObj.hostname) ||
|
!this.isBadException(xssReq.destDomain) ||
|
||||||
this.legacyExceptions.test("@" + unescape(srcUrl))
|
this.legacyExceptions.test("@" + unescape(srcUrl))
|
||||||
)) {
|
)) {
|
||||||
logEx("Legacy exception", this.legacyExceptions);
|
logEx("Legacy exception", this.legacyExceptions);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!srcObj && isGet) {
|
if (!srcOrigin && isGet) {
|
||||||
if (/^https?:\/\/msdn\.microsoft\.com\/query\/[^<]+$/.test(unescapedDest)) {
|
if (/^https?:\/\/msdn\.microsoft\.com\/query\/[^<]+$/.test(unescapedDest)) {
|
||||||
return true; // MSDN from Microsoft VS
|
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 () => {
|
XSS.InjectionChecker = (async () => {
|
||||||
await include([
|
await include([
|
||||||
|
"/common/SyntaxChecker.js",
|
||||||
"/lib/Base64.js",
|
"/lib/Base64.js",
|
||||||
"/lib/Timing.js",
|
"/lib/Timing.js",
|
||||||
"/xss/FlashIdiocy.js",
|
"/xss/FlashIdiocy.js",
|
||||||
|
@ -1031,7 +1031,7 @@ XSS.InjectionChecker = (async () => {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (s.indexOf("&") !== -1) {
|
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;
|
if (unent !== s && await this._checkRecursive(unent, depth)) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1050,7 +1050,7 @@ XSS.InjectionChecker = (async () => {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (/[\u0000-\u001f]|&#/.test(unescaped)) {
|
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)) {
|
if (unescaped != unent && await this._checkRecursive(unent, depth)) {
|
||||||
this.log("Trash-stripped nested URL match!");
|
this.log("Trash-stripped nested URL match!");
|
||||||
return true;
|
return true;
|
||||||
|
|
120
src/xss/XSS.js
120
src/xss/XSS.js
|
@ -4,8 +4,8 @@ var XSS = (() => {
|
||||||
|
|
||||||
const ABORT = {cancel: true}, ALLOW = {};
|
const ABORT = {cancel: true}, ALLOW = {};
|
||||||
|
|
||||||
|
let workersMap = new Map();
|
||||||
let promptsMap = new Map();
|
let promptsMap = new Map();
|
||||||
let timingsMap = new Map();
|
|
||||||
|
|
||||||
async function getUserResponse(xssReq) {
|
async function getUserResponse(xssReq) {
|
||||||
let {originKey} = xssReq;
|
let {originKey} = xssReq;
|
||||||
|
@ -23,10 +23,11 @@ var XSS = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function doneListener(request) {
|
function doneListener(request) {
|
||||||
let timing = timingsMap.get(request.id);
|
let {requestId} = request;
|
||||||
if (timing) {
|
let worker = workersMap.get(requestId);
|
||||||
timing.interrupted = true;
|
if (worker) {
|
||||||
timingsMap.delete(request.id);
|
worker.terminate();
|
||||||
|
workersMap.delete(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ var XSS = (() => {
|
||||||
data = [];
|
data = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e, "XSS filter processing %o", xssReq);
|
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
|
// we don't want prompts if the request expired / errored first
|
||||||
return ABORT;
|
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 {
|
return {
|
||||||
async start() {
|
async start() {
|
||||||
if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only
|
if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only
|
||||||
|
@ -166,7 +182,6 @@ var XSS = (() => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
parseRequest(request) {
|
parseRequest(request) {
|
||||||
let {
|
let {
|
||||||
url: destUrl,
|
url: destUrl,
|
||||||
|
@ -175,7 +190,7 @@ var XSS = (() => {
|
||||||
} = request;
|
} = request;
|
||||||
let destObj;
|
let destObj;
|
||||||
try {
|
try {
|
||||||
destObj = new URL(destUrl);
|
destObj = parseUrl(destUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e, "Cannot create URL object for %s", destUrl);
|
error(e, "Cannot create URL object for %s", destUrl);
|
||||||
return null;
|
return null;
|
||||||
|
@ -183,7 +198,7 @@ var XSS = (() => {
|
||||||
let srcObj = null;
|
let srcObj = null;
|
||||||
if (srcUrl) {
|
if (srcUrl) {
|
||||||
try {
|
try {
|
||||||
srcObj = new URL(srcUrl);
|
srcObj = parseUrl(srcUrl);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
} else {
|
} else {
|
||||||
srcUrl = "";
|
srcUrl = "";
|
||||||
|
@ -198,28 +213,21 @@ var XSS = (() => {
|
||||||
|
|
||||||
let isGet = method === "GET";
|
let isGet = method === "GET";
|
||||||
return {
|
return {
|
||||||
xssUnparsed: request,
|
unparsedRequest: request,
|
||||||
srcUrl,
|
srcUrl,
|
||||||
destUrl,
|
destUrl,
|
||||||
srcObj,
|
srcObj,
|
||||||
destObj,
|
destObj,
|
||||||
srcOrigin,
|
srcOrigin,
|
||||||
destOrigin,
|
destOrigin,
|
||||||
get srcDomain() {
|
srcDomain: srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "",
|
||||||
delete this.srcDomain;
|
destDomain: tld.getDomain(destObj.hostname),
|
||||||
return this.srcDomain = srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "";
|
originKey: `${srcOrigin}>${destOrigin}`,
|
||||||
},
|
|
||||||
get destDomain() {
|
|
||||||
delete this.destDomain;
|
|
||||||
return this.destDomain = tld.getDomain(destObj.hostname);
|
|
||||||
},
|
|
||||||
get originKey() {
|
|
||||||
delete this.originKey;
|
|
||||||
return this.originKey = `${srcOrigin}>${destOrigin}`;
|
|
||||||
},
|
|
||||||
unescapedDest,
|
unescapedDest,
|
||||||
isGet,
|
isGet,
|
||||||
isPost: !isGet && method === "POST",
|
isPost: !isGet && method === "POST",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
debugging: ns.local.debug,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -237,42 +245,46 @@ var XSS = (() => {
|
||||||
return this._userChoices[originKey];
|
return this._userChoices[originKey];
|
||||||
},
|
},
|
||||||
|
|
||||||
async maybe(request) { // return reason or null if everything seems fine
|
async maybe(xssReq) { // return reason or null if everything seems fine
|
||||||
let xssReq = request.xssUnparsed ? request : this.parseRequest(request);
|
|
||||||
request = xssReq.xssUnparsed;
|
|
||||||
|
|
||||||
if (await this.Exceptions.shouldIgnore(xssReq)) {
|
if (await this.Exceptions.shouldIgnore(xssReq)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let skip = this.Exceptions.partial(xssReq);
|
||||||
skipParams,
|
let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js"));
|
||||||
skipRx
|
let {requestId} = xssReq.unparsedRequest;
|
||||||
} = this.Exceptions.partial(xssReq);
|
workersMap.set(requestId, worker)
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
let {destUrl} = xssReq;
|
let cleanup = () => {
|
||||||
|
workersMap.delete(requestId);
|
||||||
await include("/xss/InjectionChecker.js");
|
worker.terminate();
|
||||||
let ic = new (await this.InjectionChecker)();
|
};
|
||||||
ic.logEnabled = ns.local.debug;
|
worker.onmessage = e => {
|
||||||
let {timing} = ic;
|
let {data} = e;
|
||||||
timingsMap.set(request.id, timing);
|
if (data) {
|
||||||
timing.fatalTimeout = true;
|
if (data.logType) {
|
||||||
|
window[data.logType](...data.log);
|
||||||
let postInjection = xssReq.isPost &&
|
return;
|
||||||
request.requestBody && request.requestBody.formData &&
|
}
|
||||||
await ic.checkPost(request.requestBody.formData, skipParams);
|
if (data.error) {
|
||||||
|
reject(data.error);
|
||||||
if (timing.tooLong) {
|
cleanup();
|
||||||
log("[XSS] Long check (%s ms) - %s", timing.elapsed, JSON.stringify(xssReq));
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let protectName = ic.nameAssignment;
|
resolve(e.data);
|
||||||
let urlInjection = await ic.checkUrl(destUrl, skipRx);
|
cleanup();
|
||||||
protectName = protectName || ic.nameAssignment;
|
}
|
||||||
|
worker.onerror = worker.onmessageerror = e => {
|
||||||
return !(protectName || postInjection || urlInjection) ? null
|
reject(e);
|
||||||
: { protectName, postInjection, urlInjection };
|
cleanup();
|
||||||
|
}
|
||||||
|
worker.postMessage({handler: "check", xssReq, skip});
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error("Timeout! DOS attack attempt?"));
|
||||||
|
cleanup();
|
||||||
|
}, 20000)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
Loading…
Reference in New Issue