XSS Filter made further asynchronous, prevents freezes on complex JSON payloads.

This commit is contained in:
hackademix 2019-05-27 00:28:01 +02:00
parent 4826128e43
commit 5597c4b0e5
2 changed files with 147 additions and 123 deletions

View File

@ -2,6 +2,7 @@ debug("Initializing InjectionChecker");
XSS.InjectionChecker = (async () => {
await include([
"/lib/Base64.js",
"/lib/Timing.js",
"/xss/FlashIdiocy.js",
"/xss/ASPIdiocy.js"]
);
@ -26,22 +27,24 @@ XSS.InjectionChecker = (async () => {
"\\b(?:" + IC_EVENT_PATTERN + ")[^]*=[^]*\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b" +
"|\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b[^]+\\b(?:" + IC_EVENT_PATTERN + ")[^]*=";
var InjectionChecker = {
reset: function() {
function InjectionChecker() {
this.timing = new Timing();
this.reset();
}
InjectionChecker.prototype = {
reset() {
this.isPost =
this.base64 =
this.nameAssignment = false;
this.base64tested = [];
},
fuzzify: fuzzify,
syntax: new SyntaxChecker(),
_log: function(msg, t, i) {
_log: function(msg, i) {
if (msg) msg = this._printable(msg);
if (t) msg += " - TIME: " + (Date.now() - t);
if (t) msg += " - TIME: " + (this.timing.elapsed);
if (i) msg += " - ITER: " + i;
debug("[InjectionChecker]", msg);
},
@ -164,22 +167,28 @@ XSS.InjectionChecker = (async () => {
s;
},
reduceJSON(s) {
async reduceJSON(s) {
const REPL = 'J';
const toStringRx = /^function\s*toString\(\)\s*{\s*\[native code\]\s*\}$/;
// optimistic case first, one big JSON block
let m = s.match(/{[^]+}|\[[^]*{[^]+}[^]*\]/);
let m = s.match(/{[^]+}|\[\s*{[^]+}\s*\]/);
if (!m) return s;
// semicolon-separated JSON chunks, like on syndication.twitter.com
if (/}\s*;\s*{/.test(s)) s = s.split(";").map(chunk => this.reduceJSON(chunk)).join(";");
if (/}\s*;\s*{/.test(s)) {
let chunks = [];
for (let chunk of s.split(";")) {
chunks.push(await this.reduceJSON(chunk));
}
s = chunks.join(";");
}
let [expr] = m;
try {
if (toStringRx.test(JSON.parse(expr).toString)) {
this.log("Reducing big JSON " + expr);
return this.reduceJSON(s.replace(expr, REPL));
return await this.reduceJSON(s.replace(expr, REPL));
}
} catch (e) {}
@ -188,9 +197,16 @@ XSS.InjectionChecker = (async () => {
let start = s.indexOf("{");
let end = s.lastIndexOf("}");
let prevExpr = "";
let iterations = 0;
while (start > -1 && end - start > 1) {
expr = s.substring(start, end + 1);
let before = s.substring(0, start);
let after = s.substring(end + 1);
if (expr === prevExpr) break;
iterations++;
if (await this.timing.pause()) {
this.log(`JSON reduction iterations ${iterations++}, elapsed ${this.timing.elapsed}, expr ${expr}`);
}
end = s.lastIndexOf("}", end - 1);
if (end === -1) {
start = s.indexOf("{", start + 1);
@ -201,7 +217,7 @@ XSS.InjectionChecker = (async () => {
continue;
this.log("Reducing JSON " + expr);
s = s.replace(expr, REPL);
s = `${before}${REPL}${after}`;
break;
} catch (e) {}
@ -212,7 +228,7 @@ XSS.InjectionChecker = (async () => {
let qred = this.reduceQuotes(expr);
if (/\{(?:\s*(?:(?:\w+:)+\w+)+;\s*)+\}/.test(qred)) {
this.log("Reducing pseudo-JSON " + expr);
s = s.replace(expr, REPL);
s = `${before}${REPL}${after}`;
break;
}
@ -220,7 +236,7 @@ XSS.InjectionChecker = (async () => {
this.checkJSSyntax("JSON = " + qred) // no-assignment JSON fails with "invalid label"
) {
this.log("Reducing slow JSON " + expr);
s = s.replace(expr, REPL);
s = `${before}${REPL}${after}`;
break;
}
prevExpr = expr;
@ -304,7 +320,7 @@ XSS.InjectionChecker = (async () => {
_dotRx: /\./g,
_removeDotsRx: /^openid\.[\w.-]+(?==)|(?:[?&#\/]|^)[\w.-]+(?=[\/\?&#]|$)|[\w\.]*\.(?:\b[A-Z]+|\w*\d|[a-z][$_])[\w.-]*|=[a-z.-]+\.(?:com|net|org|biz|info|xxx|[a-z]{2})(?:[;&/]|$)/g,
_removeDots: (p) => p.replace(InjectionChecker._dotRx, '|'),
_removeDots(p) { return p.replace(this._dotRx, '|'); },
_arrayAccessRx: /\s*\[\d+\]/g,
_riskyOperatorsRx: /[+-]{2}\s*(?:\/[*/][\s\S]+)?(?:\w+(?:\/[*/][\s\S]+)?[[.]|location)|(?:\]|\.\s*(?:\/[*/][\s\S]+)?\w+|location)\s*(?:\/[*/][\s\S]+)?([+-]{2}|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)/, // inc/dec/self-modifying assignments on DOM props
_assignmentRx: /^(?:[^()="'\s]+=(?:[^(='"\[+]+|[?a-zA-Z_0-9;,&=/]+|[\d.|]+))$/,
@ -484,16 +500,14 @@ XSS.InjectionChecker = (async () => {
return this.invalidCharsRx = new RegExp("^[^\"'`/<>]*[" + this._createInvalidRanges() + "]");
},
checkJSBreak: function InjectionChecker_checkJSBreak(s) {
async checkJSBreak(s) {
// Direct script injection breaking JS string literals or comments
// cleanup most urlencoded noise and reduce JSON/XML
s = ';' + this.reduceXML(this.reduceJSON(this.collapseChars(
// preliminarily cleanup most urlencoded noise and reduce JSON/XML
s = ';' + this.reduceXML(await this.reduceJSON(this.collapseChars(
s.replace(/\%\d+[a-z\(]\w*/gi, '§')
.replace(/[\r\n\u2028\u2029]+/g, "\n")
.replace(/[\x01-\x09\x0b-\x20]+/g, ' ')
)));
))).replace(/[\r\n\u2028\u2029]+/g, "\n");
if (s.indexOf("*/") > 0 && /\*\/[\s\S]+\/\*/.test(s)) { // possible scrambled multi-point with comment balancing
s += ';' + s.match(/\*\/[\s\S]+/);
@ -501,8 +515,7 @@ XSS.InjectionChecker = (async () => {
if (!this.maybeJS(s)) return false;
const MAX_TIME = 8000,
MAX_LOOPS = 1200;
const MAX_LOOPS = 1200;
const logEnabled = this.logEnabled;
@ -519,8 +532,7 @@ XSS.InjectionChecker = (async () => {
const injectionFinderRx = /(['"`#;>:{}]|[/?=](?![?&=])|&(?![\w-.[\]&!-]*=)|\*\/)(?!\1)/g;
injectionFinderRx.lastIndex = 0;
const t = Date.now();
var iterations = 0;
let iterations = 0;
for (let dangerPos = 0, m;
(m = injectionFinderRx.exec(s));) {
@ -540,7 +552,7 @@ XSS.InjectionChecker = (async () => {
let quote = breakSeq in this.breakStops ? breakSeq : '';
if (!this.maybeJS(quote ? quote + subj : subj)) {
this.log("Fast escape on " + subj, t, iterations);
this.log("Fast escape on " + subj, iterations);
return false;
}
@ -548,7 +560,7 @@ XSS.InjectionChecker = (async () => {
if (script.length < subj.length) {
if (!this.maybeJS(script)) {
this.log("Skipping to first nested URL in " + subj, t, iterations);
this.log("Skipping to first nested URL in " + subj, iterations);
injectionFinderRx.lastIndex += subj.indexOf("://") + 1;
continue;
}
@ -581,10 +593,8 @@ XSS.InjectionChecker = (async () => {
let bs = this.breakStops[quote || 'nq']
for (let len = expr.length, moved = false, hunt = !!expr, lastExpr = ''; hunt;) {
if (Date.now() - t > MAX_TIME) {
this.log("Too long execution time! Assuming DOS... " + (Date.now() - t), t, iterations);
return true;
if (await this.timing.pause()) {
this.log(`Elapsed ${this.timing.elapsed}ms, taken a ${this.timing.pauseTime}ms nap.`)
}
hunt = expr.length < subj.length;
@ -626,7 +636,7 @@ XSS.InjectionChecker = (async () => {
if (quote) {
if (this.checkNonTrivialJSSyntax(expr)) {
this.log("Non-trivial JS inside quoted string detected", t, iterations);
this.log("Non-trivial JS inside quoted string detected", iterations);
return true;
}
script = this.syntax.unquote(quote + expr, quote);
@ -636,7 +646,7 @@ XSS.InjectionChecker = (async () => {
/"./.test(script) && this.checkNonTrivialJSSyntax('""' + script + '"')
) && this.checkLastFunction()
) {
this.log("JS quote Break Injection detected", t, iterations);
this.log("JS quote Break Injection detected", iterations);
return true;
}
script = quote + quote + expr + quote;
@ -649,7 +659,7 @@ XSS.InjectionChecker = (async () => {
if (balanced !== script && balanced.indexOf('(') > -1) {
script = balanced + ")";
} else {
this.log("SKIP (head syntax) " + script, t, iterations);
this.log("SKIP (head syntax) " + script, iterations);
break; // unrepairable syntax error in the head, move left cursor forward
}
}
@ -657,22 +667,22 @@ XSS.InjectionChecker = (async () => {
if (this.maybeJS(this.reduceQuotes(script))) {
if (this.checkJSSyntax(script) && this.checkLastFunction()) {
this.log("JS Break Injection detected", t, iterations);
this.log("JS Break Injection detected", iterations);
return true;
}
if (this.checkTemplates(script)) {
this.log("JS template expression injection detected", t, iterations);
this.log("JS template expression injection detected", iterations);
return true;
}
if (++iterations > MAX_LOOPS) {
this.log("Too many syntax checks! Assuming DOS... " + s, t, iterations);
this.log("Too many syntax checks! Assuming DOS... " + s, iterations);
return true;
}
if (this.syntax.lastError) { // could be null if we're here thanks to checkLastFunction()
let errmsg = this.syntax.lastError.message;
if (logEnabled) this.log(errmsg + " --- " + this.syntax.lastScript + " --- ", t, iterations);
if (logEnabled) this.log(errmsg + " --- " + this.syntax.lastScript + " --- ", iterations);
if (!quote) {
if (errmsg.indexOf("left-hand") !== -1) {
let m = subj.match(/^([^\]\(\\'"=\?]+?)[\w$\u0080-\uffff\s]+[=\?]/);
@ -720,7 +730,7 @@ XSS.InjectionChecker = (async () => {
expr += char;
moved = hunt = true;
len++;
this.log("Balancing " + char, t, iterations);
this.log("Balancing " + char, iterations);
} else {
break;
}
@ -732,12 +742,12 @@ XSS.InjectionChecker = (async () => {
}
}
}
this.log(s, t, iterations);
this.log(s, iterations);
return false;
},
checkJS: function(s, unescapedUni) {
async checkJS(s, unescapedUni) {
this.log(s);
if (/[=\(](?:[\s\S]*(?:\?name\b[\s\S]*:|[^&?]\bname\b)|name\b)/.test(s)) {
@ -761,9 +771,9 @@ XSS.InjectionChecker = (async () => {
}
this.syntax.lastFunction = null;
let ret = this.checkAttributes(s) ||
(/[\\\(]|=[^=]/.test(s) || this._riskyOperatorsRx.test(s)) && this.checkJSBreak(s) || // MAIN
hasUnicodeEscapes && this.checkJS(this.unescapeJS(s), true); // optional unescaped recursion
let ret = await this.checkAttributes(s) ||
(/[\\\(]|=[^=]/.test(s) || this._riskyOperatorsRx.test(s)) && await this.checkJSBreak(s) || // MAIN
hasUnicodeEscapes && await this.checkJS(this.unescapeJS(s), true); // optional unescaped recursion
if (ret) {
let msg = "JavaScript Injection in " + s;
if (this.syntax.lastFunction) {
@ -822,14 +832,14 @@ XSS.InjectionChecker = (async () => {
"|-moz-binding[^]*:[^]*url[^]*\\(|\\{\\{[^]+\\}\\}")
.replace(/[a-rt-z\-]/g, "\\W*$&"),
"i"),
checkAttributes: function(s) {
async checkAttributes(s) {
s = this.reduceDashPlus(s);
if (this._rxCheck("Attributes", s)) return true;
if (/\\/.test(s) && this._rxCheck("Attributes", this.unescapeCSS(s))) return true;
let dataPos = s.search(/data:\S*\s/i);
if (dataPos !== -1) {
let data = this.urlUnescape(s.substring(dataPos).replace(/\s/g, ''));
if (this.checkHTML(data) || this.checkAttributes(data)) return true;
if (await this.checkHTML(data) || await this.checkAttributes(data)) return true;
}
return false;
},
@ -840,24 +850,24 @@ XSS.InjectionChecker = (async () => {
")[^>\\w])|['\"\\s\\0/](?:formaction|style|background|src|lowsrc|ping|innerhtml|data-bind|(?:data-)?mv-(?:\\w+[\\w-]*)|" + IC_EVENT_PATTERN +
")[\\s\\0]*=|<%[^]+[=(][^]+%>", "i"),
checkHTML(s) {
async checkHTML(s) {
let links = s.match(/\b(?:href|src|base|(?:form)?action|\w+-\w+)\s*=\s*(?:(["'])[\s\S]*?\1|(?:[^'">][^>\s]*)?[:?\/#][^>\s]*)/ig);
if (links) {
for (let l of links) {
l = l.replace(/[^=]*=\s*/i, '').replace(/[\u0000-\u001f]/g, '');
l = /^["']/.test(l) ? l.replace(/^(['"])([^]*?)\1[^]*/g, '$2') : l.replace(/[\s>][^]*/, '');
if (/^(?:javascript|data):|\[[^]+\]/i.test(l) || /[<'"(]/.test(unescape(l)) && this.checkUrl(l)) return true;
if (/^(?:javascript|data):|\[[^]+\]/i.test(l) || /[<'"(]/.test(unescape(l)) && await this.checkUrl(l)) return true;
}
}
return this._rxCheck("HTML", s) || this._rxCheck("Globals", s);
},
checkNoscript: function(s) {
async checkNoscript(s) {
this.log(s);
return s.indexOf("\x1b(J") !== -1 && this.checkNoscript(s.replace(/\x1b\(J/g, '')) || // ignored in iso-2022-jp
s.indexOf("\x7e\x0a") !== -1 && this.checkNoscript(s.replace(/\x7e\x0a/g, '')) || // ignored in hz-gb-2312
this.checkHTML(s) || this.checkSQLI(s) || this.checkHeaders(s);
return s.indexOf("\x1b(J") !== -1 && await this.checkNoscript(s.replace(/\x1b\(J/g, '')) || // ignored in iso-2022-jp
s.indexOf("\x7e\x0a") !== -1 && await this.checkNoscript(s.replace(/\x7e\x0a/g, '')) || // ignored in hz-gb-2312
await this.checkHTML(s) || this.checkSQLI(s) || this.checkHeaders(s);
},
HeadersChecker: /[\r\n]\s*(?:content-(?:type|encoding))\s*:/i,
@ -876,26 +886,24 @@ XSS.InjectionChecker = (async () => {
}, // exposed here just for debugging purposes
checkBase64: function(url) {
async checkBase64(url) {
this.base64 = false;
const MAX_TIME = 8000;
const DOS_MSG = "Too long execution time, assuming DOS in Base64 checks";
this.log(url);
var parts = url.split("#"); // check hash
if (parts.length > 1 && this.checkBase64FragEx(unescape(parts[1])))
if (parts.length > 1 && await this.checkBase64FragEx(unescape(parts[1])))
return true;
parts = parts[0].split(/[&;]/); // check query string
if (parts.length > 0 && parts.some(function(p) {
var pos = p.indexOf("=");
if (pos > -1) p = p.substring(pos + 1);
return this.checkBase64FragEx(unescape(p));
}, this))
return true;
for (let p of parts) {
var pos = p.indexOf("=");
if (pos > -1) p = p.substring(pos + 1);
if (await this.checkBase64FragEx(unescape(p))) {
return true;
}
}
url = parts[0];
parts = Base64.purify(url).split("/");
@ -904,47 +912,38 @@ XSS.InjectionChecker = (async () => {
return true;
}
var t = Date.now();
if (parts.some(function(p) {
if (Date.now() - t > MAX_TIME) {
this.log(DOS_MSG);
return true;
}
return this.checkBase64Frag(Base64.purify(Base64.alt(p)));
}, this))
return true;
for (let p of parts) {
if (await this.checkBase64Frag(Base64.purify(Base64.alt(p)))) {
return true;
};
await this.timing.pause();
}
var uparts = Base64.purify(unescape(url)).split("/");
t = Date.now();
while (parts.length) {
if (Date.now() - t > MAX_TIME) {
this.log(DOS_MSG);
return true;
}
if (this.checkBase64Frag(parts.join("/")) ||
this.checkBase64Frag(uparts.join("/")))
if (await this.checkBase64Frag(parts.join("/")) ||
await this.checkBase64Frag(uparts.join("/")))
return true;
parts.shift();
uparts.shift();
await this.timing.pause();
}
return false;
},
checkBase64Frag: function(f) {
async checkBase64Frag(f) {
if (this.base64tested.indexOf(f) < 0) {
this.base64tested.push(f);
try {
var s = Base64.decode(f);
if (s && s.replace(/[^\w\(\)]/g, '').length > 7 &&
(this.checkHTML(s) ||
this.checkAttributes(s))
// this.checkJS(s) // -- alternate, whose usefulness is doubious but which easily leads to DOS
(await this.checkHTML(s) ||
await this.checkAttributes(s))
// || await this.checkJS(s) // -- alternate, whose usefulness is doubious but which easily leads to DOS
) {
this.log("Detected BASE64 encoded injection: " + f + " --- (" + s + ")");
return this.base64 = true;
@ -954,14 +953,14 @@ XSS.InjectionChecker = (async () => {
return false;
},
checkBase64FragEx: function(f) {
return this.checkBase64Frag(Base64.purify(f)) || this.checkBase64Frag(Base64.purify(Base64.alt(f)));
async checkBase64FragEx(f) {
return await this.checkBase64Frag(Base64.purify(f)) || await this.checkBase64Frag(Base64.purify(Base64.alt(f)));
},
checkUrl(url, skipRx = null) {
async checkUrl(url, skipRx = null) {
if (skipRx) url = url.replace(skipRx, '');
return this.checkRecursive(url
return await this.checkRecursive(url
// assume protocol and host are safe, but keep the leading double slash to keep comments in account
.replace(/^[a-z]+:\/\/.*?(?=\/|$)/, "//")
// Remove outer parenses from ASP.NET cookieless session's AppPathModifier
@ -969,47 +968,47 @@ XSS.InjectionChecker = (async () => {
);
},
checkPost(formData, skipParams = null) {
async checkPost(formData, skipParams = null) {
let keys = Object.keys(formData);
if (Array.isArray(skipParams)) keys = keys.filter(k => !skipParams.includes(k))
for (let key of keys) {
let chunk = `${key}=${formData[key].join(`;`)}`;
if (this.checkRecursive(chunk, 2, true)) {
if (await this.checkRecursive(chunk, 2, true)) {
return chunk;
}
}
return null;
},
checkRecursive(s, depth = 3, isPost = false) {
async checkRecursive(s, depth = 3, isPost = false) {
this.reset();
this.isPost = isPost;
if (ASPIdiocy.affects(s)) {
if (this.checkRecursive(ASPIdiocy.process(s), depth, isPost))
if (await this.checkRecursive(ASPIdiocy.process(s), depth, isPost))
return true;
} else if (ASPIdiocy.hasBadPercents(s) && this.checkRecursive(ASPIdiocy.removeBadPercents(s), depth, isPost)) {
} else if (ASPIdiocy.hasBadPercents(s) && await this.checkRecursive(ASPIdiocy.removeBadPercents(s), depth, isPost)) {
return true;
}
if (FlashIdiocy.affects(s)) {
let purged = FlashIdiocy.purgeBadEncodings(s);
if (purged !== s && this.checkRecursive(purged, depth, isPost))
if (purged !== s && await this.checkRecursive(purged, depth, isPost))
return true;
let decoded = FlashIdiocy.platformDecode(purged);
if (decoded !== purged && this.checkRecursive(decoded, depth, isPost))
if (decoded !== purged && await this.checkRecursive(decoded, depth, isPost))
return true;
}
if (!isPost && s.indexOf("coalesced:") !== 0) {
let coalesced = ASPIdiocy.coalesceQuery(s);
if (coalesced !== s && this.checkRecursive("coalesced:" + coalesced, depth, isPost))
if (coalesced !== s && await this.checkRecursive("coalesced:" + coalesced, depth, isPost))
return true;
}
if (isPost) {
s = this.formUnescape(s);
if (this.checkBase64Frag(Base64.purify(s))) return true;
if (await this.checkBase64Frag(Base64.purify(s))) return true;
if (s.indexOf("<") > -1) {
// remove XML-embedded Base64 binary data
@ -1018,27 +1017,29 @@ XSS.InjectionChecker = (async () => {
s = "#" + s;
} else {
if (this.checkBase64(s.replace(/^\/{1,3}/, ''))) return true;
if (await this.checkBase64(s.replace(/^\/{1,3}/, ''))) return true;
}
if (isPost) s = "#" + s; // allows the string to be JS-checked as a whole
return this._checkRecursive(s, depth);
return await this._checkRecursive(s, depth);
},
_checkRecursive: function(s, depth) {
async _checkRecursive(s, depth) {
if (this.checkHTML(s) || this.checkJS(s) || this.checkSQLI(s) || this.checkHeaders(s))
if (await this.checkHTML(s) || await this.checkJS(s) || this.checkSQLI(s) || this.checkHeaders(s))
return true;
if (s.indexOf("&") !== -1) {
let unent = Entities.convertAll(s);
if (unent !== s && this._checkRecursive(unent, depth)) return true;
if (unent !== s && await this._checkRecursive(unent, depth)) return true;
}
if (--depth <= 0)
return false;
if (s.indexOf('+') !== -1 && this._checkRecursive(this.formUnescape(s), depth))
await this.timing.pause()
if (s.indexOf('+') !== -1 && await this._checkRecursive(this.formUnescape(s), depth))
return true;
var unescaped = this.urlUnescape(s);
@ -1049,7 +1050,7 @@ XSS.InjectionChecker = (async () => {
if (/[\u0000-\u001f]|&#/.test(unescaped)) {
let unent = Entities.convertAll(unescaped.replace(/[\u0000-\u001f]+/g, ''));
if (unescaped != unent && this._checkRecursive(unent, depth)) {
if (unescaped != unent && await this._checkRecursive(unent, depth)) {
this.log("Trash-stripped nested URL match!");
return true;
}
@ -1057,14 +1058,14 @@ XSS.InjectionChecker = (async () => {
if (/\\x[0-9a-f]/i.test(unescaped)) {
let literal = this.unescapeJSLiteral(unescaped);
if (unescaped !== literal && this._checkRecursive(literal, depth)) {
if (unescaped !== literal && await this._checkRecursive(literal, depth)) {
this.log("Escaped literal match!");
return true;
}
}
if (unescaped.indexOf("\x1b(J") !== -1 && this._checkRecursive(unescaped.replace(/\x1b\(J/g, ''), depth) || // ignored in iso-2022-jp
unescaped.indexOf("\x7e\x0a") !== -1 && this._checkRecursive(unescaped.replace(/\x7e\x0a/g, '')) // ignored in hz-gb-2312
if (unescaped.indexOf("\x1b(J") !== -1 && await this._checkRecursive(unescaped.replace(/\x1b\(J/g, ''), depth) || // ignored in iso-2022-jp
unescaped.indexOf("\x7e\x0a") !== -1 && await this._checkRecursive(unescaped.replace(/\x7e\x0a/g, '')) // ignored in hz-gb-2312
) {
return true;
}
@ -1072,16 +1073,16 @@ XSS.InjectionChecker = (async () => {
if (badUTF8) {
try {
let legacyEscaped = unescape(unescaped);
if (legacyEscaped !== unescaped && this._checkRecursive(unescape(unescaped))) return true;
if (legacyEscaped !== unescaped && await this._checkRecursive(unescape(unescaped))) return true;
} catch (e) {}
}
if (unescaped !== s && this._checkRecursive(unescaped, depth)) {
if (unescaped !== s && await this._checkRecursive(unescaped, depth)) {
return true;
}
s = this.ebayUnescape(unescaped);
if (s != unescaped && this._checkRecursive(s, depth))
if (s != unescaped && await this._checkRecursive(s, depth))
return true;
return false;
@ -1155,7 +1156,7 @@ XSS.InjectionChecker = (async () => {
});
},
checkWindowName(window, url) {
async checkWindowName(window, url) {
var originalAttempt = window.name;
try {
if (/^https?:\/\/(?:[^/]*\.)?\byimg\.com\/rq\/darla\//.test(url)) {
@ -1170,7 +1171,7 @@ XSS.InjectionChecker = (async () => {
} catch (e) {}
}
if (/[%=\(\\<]/.test(originalAttempt) && InjectionChecker.checkUrl(originalAttempt)) {
if (/[%=\(\\<]/.test(originalAttempt) && await this.checkUrl(originalAttempt)) {
window.name = originalAttempt.replace(/[%=\(\\<]/g, " ");
}
@ -1178,7 +1179,7 @@ XSS.InjectionChecker = (async () => {
try {
if ((originalAttempt.length % 4 === 0)) {
var bin = window.atob(window.name);
if (/[%=\(\\]/.test(bin) && InjectionChecker.checkUrl(bin)) {
if (/[%=\(\\]/.test(bin) && await this.checkUrl(bin)) {
window.name = "BASE_64_XSS";
}
}

View File

@ -5,6 +5,7 @@ var XSS = (() => {
const ABORT = {cancel: true}, ALLOW = {};
let promptsMap = new Map();
let timingsMap = new Map();
async function getUserResponse(xssReq) {
let {originKey} = xssReq;
@ -21,6 +22,14 @@ var XSS = (() => {
return null;
}
function doneListener(request) {
let timing = timingsMap.get(request.id);
if (timing) {
timing.interrupted = true;
timingsMap.delete(request.id);
}
}
async function requestListener(request) {
if (ns.isEnforced(request.tabId)) {
@ -40,19 +49,24 @@ var XSS = (() => {
let data;
let reasons;
try {
reasons = await XSS.maybe(xssReq);
if (!reasons) return ALLOW;
data = [];
} catch (e) {
error(e, "XSS filter processing %o", xssReq);
if (e instanceof TimingException) {
// we don't want prompts if the request expired / errored first
return;
}
reasons = { urlInjection: true };
data = [e.toString()];
}
let prompting = (async () => {
userResponse = await getUserResponse(xssReq);
if (userResponse) return userResponse;
@ -115,7 +129,7 @@ var XSS = (() => {
async start() {
if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only
let {onBeforeRequest} = browser.webRequest;
let {onBeforeRequest, onCompleted, onErrorOccurred} = browser.webRequest;
if (onBeforeRequest.hasListener(requestListener)) return;
@ -134,11 +148,15 @@ var XSS = (() => {
}
XSS.Exceptions.setWhitelist(null);
}
onBeforeRequest.addListener(requestListener, {
let filter = {
urls: ["*://*/*"],
types: ["main_frame", "sub_frame", "object"]
}, ["blocking", "requestBody"]);
};
onBeforeRequest.addListener(requestListener, filter, ["blocking", "requestBody"]);
if (!onCompleted.hasListener(doneListener)) {
onCompleted.addListener(doneListener, filter);
onErrorOccurred.addListener(doneListener, filter);
}
},
stop() {
@ -235,17 +253,22 @@ var XSS = (() => {
let {destUrl} = xssReq;
await include("/xss/InjectionChecker.js");
let ic = await this.InjectionChecker;
ic.reset();
let ic = new (await this.InjectionChecker)();
let {timing} = ic;
timingsMap.set(request.id, timing);
let postInjection = xssReq.isPost &&
request.requestBody && request.requestBody.formData &&
ic.checkPost(request.requestBody.formData, skipParams);
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 = ic.checkUrl(destUrl, skipRx);
let urlInjection = await ic.checkUrl(destUrl, skipRx);
protectName = protectName || ic.nameAssignment;
ic.reset();
return !(protectName || postInjection || urlInjection) ? null
: { protectName, postInjection, urlInjection };
}