From 5597c4b0e53b27ea479383f4a890837417e6cc15 Mon Sep 17 00:00:00 2001 From: hackademix Date: Mon, 27 May 2019 00:28:01 +0200 Subject: XSS Filter made further asynchronous, prevents freezes on complex JSON payloads. --- src/xss/InjectionChecker.js | 227 ++++++++++++++++++++++---------------------- src/xss/XSS.js | 43 +++++++-- 2 files changed, 147 insertions(+), 123 deletions(-) diff --git a/src/xss/InjectionChecker.js b/src/xss/InjectionChecker.js index 27ae50b..d750232 100644 --- a/src/xss/InjectionChecker.js +++ b/src/xss/InjectionChecker.js @@ -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"; } } diff --git a/src/xss/XSS.js b/src/xss/XSS.js index 93230cd..51216d8 100644 --- a/src/xss/XSS.js +++ b/src/xss/XSS.js @@ -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 }; } -- cgit v1.2.3