debug("Initializing InjectionChecker"); XSS.InjectionChecker = (async () => { await include([ "/lib/Base64.js", "/lib/Timing.js", "/xss/FlashIdiocy.js", "/xss/ASPIdiocy.js"] ); var {FlashIdiocy, ASPIdiocy} = XSS; const wordCharRx = /\w/g; function fuzzify(s) { return s.replace(wordCharRx, '\\W*(?:/[*/][^]*)?$&'); } const IC_COMMENT_PATTERN = '\\s*(?:\\/[\\/\\*][^]+)?'; const IC_WINDOW_OPENER_PATTERN = fuzzify("alert|confirm|prompt|open(?:URL)?|print|show") + "\\w*" + fuzzify("Dialog"); const IC_EVAL_PATTERN = "\\b(?:" + fuzzify('eval|import|set(?:Timeout|Interval)|(?:f|F)unction|Script|toString|Worker|document|constructor|generateCRMFRequest|jQuery|fetch|write(?:ln)?|__(?:define(?:S|G)etter|noSuchMethod)__|definePropert(?:y|ies)') + "|\\$|" + IC_WINDOW_OPENER_PATTERN + ")\\b"; const IC_EVENT_PATTERN = "on(?:m(?:o(?:z(?:browser(?:beforekey(?:down|up)|afterkey(?:down|up))|(?:network(?:down|up)loa|accesskeynotfoun)d|pointerlock(?:change|error)|(?:orientation|time)change|fullscreen(?:change|error)|visual(?:resize|scroll)|interrupt(?:begin|end)|key(?:down|up)onplugin)|use(?:l(?:ongtap|eave)|o(?:ver|ut)|enter|wheel|down|move|up))|a(?:p(?:se(?:tmessagestatus|ndmessage)|message(?:slisting|update)|folderlisting|getmessage)req|rk)|e(?:rchantvalidation|ssage(?:error)?)|(?:idimessag|ut)e)|p(?:o(?:inter(?:l(?:ock(?:change|error)|eave)|o(?:ver|ut)|cancel|enter|down|move|up)|p(?:up(?:hid(?:den|ing)|show(?:ing|n)|positioned)|state))|a(?:i(?:ring(?:con(?:firmation|sent)req|aborted)|nt)|(?:y(?:mentmethod|erdetail)chang|st|us)e|ge(?:hide|show))|u(?:ll(?:vcard(?:listing|entry)|phonebook)req|sh(?:subscriptionchange)?)|(?:[is]|ending|ty)change|ro(?:cessorerror|gress)|lay(?:ing)?|hoto)|c(?:o(?:n(?:nect(?:i(?:on(?:statechanged|available)|ng)|ed)?|t(?:rollerchange|extmenu))|m(?:p(?:osition(?:update|start|end)|lete)|mand(?:update)?)|py)|h(?:a(?:r(?:ging(?:time)?change|acteristicchanged)|nge)|ecking)|a(?:n(?:play(?:through)?|cel)|(?:llschang|ch)ed|rdstatechange)|u(?:rrent(?:channel|source)changed|echange|t)|l(?:i(?:rmodechange|ck)|ose)|fstatechange)|s(?:t(?:a(?:t(?:uschanged|echange)|lled|rt)|o(?:rage(?:areachanged)?|p)|k(?:sessione|comma)nd)|e(?:lect(?:ionchange|start)?|ek(?:ing|ed)|n(?:ding|t)|t)|ou(?:rce(?:(?:clos|end)ed|open)|nd(?:start|end))|c(?:(?:anningstate|ostatus)changed|roll)|pe(?:akerforcedchange|ech(?:start|end))|h(?:ipping(?:address|option)change|ow)|u(?:ccess|spend|bmit))|d(?:e(?:vice(?:p(?:roximity|aired)|(?:orienta|mo)tion|(?:unpaire|foun)d|change|light)|l(?:ivery(?:success|error)|eted))|i(?:s(?:c(?:hargingtimechange|onnect(?:ing|ed)?)|playpasskeyreq|abled)|aling)|r(?:a(?:g(?:e(?:n(?:ter|d)|xit)|(?:gestur|leav)e|start|drop|over)?|in)|op)|ata(?:(?:availabl|chang)e|error)?|urationchange|ownloading|blclick)|a(?:n(?:imation(?:iteration|cancel|start|end)|tennaavailablechange)|d(?:d(?:sourcebuffer|track)|apter(?:remov|add)ed)|ttribute(?:(?:write|read)req|changed)|u(?:dio(?:process|start|end)|xclick)|b(?:solutedeviceorientation|ort)|(?:2dpstatuschang|ppinstall)ed|fter(?:scriptexecute|print)|ctiv(?:estatechanged|ate)|lerting)|r(?:e(?:s(?:ourcetimingbufferfull|u(?:m(?:ing|e)|lt)|ponseprogress|ize|et)|mo(?:ve(?:sourcebuffer|track)?|te(?:resume|hel)d)|ad(?:y(?:statechange)?|success|error)|quest(?:mediaplaystatu|progres)s|(?:jectionhandl|ceiv)ed|pea(?:tEven)?t|loadpage|trieving)|(?:(?:adiost)?ate|t)change|ds(?:dis|en)abled)|Moz(?:S(?:wipeGesture(?:(?:May)?Start|Update|End)?|crolledAreaChanged)|M(?:agnifyGesture(?:Update|Start)?|ouse(?:PixelScroll|Hittest))|EdgeUI(?:C(?:omplet|ancel)|Start)ed|RotateGesture(?:Update|Start)?|(?:Press)?TapGesture|AfterPaint)|w(?:eb(?:kit(?:Animation(?:Iteration|Start|End)|animation(?:iteration|start|end)|(?:TransitionE|transitione)nd)|socket)|a(?:iting(?:forkey)?|rning)|heel)|DOM(?:Node(?:Inserted(?:IntoDocument)?|Removed(?:FromDocument)?)|(?:CharacterData|Subtree)Modified|A(?:ttrModified|ctivate)|Focus(?:Out|In)|MouseScroll)|b(?:e(?:fore(?:(?:evicte|unloa)d|p(?:aste|rint)|scriptexecute|c(?:opy|ut))|gin(?:Event)?)|u(?:fferedamountlow|sy)|oun(?:dary|ce)|l(?:ocked|ur)|roadcast)|v(?:rdisplay(?:(?:presentchang|activat)e|d(?:eactivate|isconnect)|connect)|o(?:ice(?:schanged|change)|lumechange)|(?:isibility|ersion)change)|u(?:p(?:date(?:(?:fou|e)nd|ready|start)?|gradeneeded)|n(?:handledrejection|derflow|load|mute)|s(?:erproximity|sdreceived))|e(?:n(?:ter(?:pincodereq)?|(?:crypt|abl)ed|d(?:Event|ed)?)|m(?:ergencycbmodechange|ptied)|(?:itbroadcas|vic)ted|rror|xit)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|ransition(?:cancel|start|end|run)|ime(?:update|out)|e(?:rminate|xt)|ypechange)|l(?:o(?:ad(?:e(?:d(?:meta)?data|nd)|ing(?:error|done)?|start)?|stpointercapture)|(?:anguage|evel)change)|o(?:(?:(?:rientation|tastatus)chang|(?:ff|n)lin)e|b(?:expasswordreq|solete)|verflow(?:changed)?|pen)|g(?:amepad(?:(?:dis)?connected|button(?:down|up)|axismove)|(?:otpointercaptur|roupchang)e|et)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?|requencychange|(?:inis|etc)h|ailed)|i(?:cc(?:(?:info)?change|(?:un)?detected)|n(?:coming|stall|valid|put))|h(?:(?:fp|id)statuschanged|e(?:adphoneschange|ld)|ashchange|olding)|n(?:o(?:tificationcl(?:ick|ose)|update|match)|ewrdsgroup)|Check(?:KeyPressEventModel|boxStateChange)|SVG(?:(?:Unl|L)oad|Resize|Scroll|Zoom)|key(?:statuseschange|press|down|up)|R(?:adioStateChange|equest)|(?:AppComman|Loa)d|zoom)" // autogenerated from Mozilla's source code, see html5_events/html5_events.pl ; const IC_EVENT_DOS_PATTERN = "\\b(?:" + IC_EVENT_PATTERN + ")[^]*=[^]*\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b" + "|\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b[^]+\\b(?:" + IC_EVENT_PATTERN + ")[^]*="; 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, i) { if (msg) msg = this._printable(msg); if (t) msg += " - TIME: " + (this.timing.elapsed); if (i) msg += " - ITER: " + i; debug("[InjectionChecker]", msg); }, _printable: function(msg) { return msg.toString().replace(/[^\u0020-\u007e]/g, function(s) { return "{" + s.charCodeAt(0).toString(16) + "}"; }); }, log: function() {}, get logEnabled() { return this.log == this._log; }, set logEnabled(v) { this.log = v ? this._log : function() {}; }, escalate: function(msg) { this.log(msg); log("[InjectionChecker] ", msg); }, bb: function(brac, s, kets) { for (var j = 3; j-- > 0;) { s = brac + s + kets; if (this.checkJSSyntax(s)) return true; } return false; }, checkJSSyntax(s) { // bracket balancing for micro injections like "''), e v a l (name,''" if (/^(?:''|"")?[^\('"]*\)/.test(s)) return this.bb("x(\n", s, "\n)"); if (/^(?:''|"")?[^\['"]*\\]/.test(s)) return this.bb("y[\n", s, "\n]"); if (/^(?:''|"")?[^\{'"]*\}/.test(s)) return this.bb("function z() {\n", s, "\n}"); let syntax = this.syntax; s += " /* COMMENT_TERMINATOR */\nDUMMY_EXPR"; if (syntax.check(s)) { this.log("Valid fragment " + s); return true; } return false; }, checkTemplates(script) { let templateExpressions = script.replace(/[[\]{}]/g, ";"); return templateExpressions !== script && (this.maybeMavo(script) || (this.maybeJS(templateExpressions, true) && (this.syntax.check(templateExpressions) || /[^><=]=[^=]/.test(templateExpressions) && this.syntax.check( templateExpressions.replace(/([^><=])=(?=[^=])/g, '$1==')) ))); }, maybeMavo(s) { return /\[[^]*\([^]*\)[^]*\]/.test(s) && /\b(?:and|or|mod|\$url\b)/.test(s) && this.maybeJS(s.replace(/\b(?:and|or|mod|[[\]])/g, ',').replace(/\$url\b/g, 'location'), true); }, get breakStops() { var def = "\\/\\?&#;\\s\\x00}<>"; // we stop on URL, JS and HTML delimiters var bs = { nq: new RegExp("[" + def + "]") }; for (let c of ['"', '"', '`']) { // special treatment for quotes bs[c] = new RegExp("[" + def + c + "]"); } delete this.breakStops; return (this.breakStops = bs); }, collapseChars: (s) => s.replace(/\;+/g, ';').replace(/\/{4,}/g, '////') .replace(/\s+/g, (s) => /\n/g.test(s) ? '\n' : ' '), _reduceBackslashes: (bs) => bs.length % 2 ? "\\" : "", reduceQuotes: function(s) { if (s[0] == '/') { // reduce common leading path fragment resembling a regular expression or a comment s = s.replace(/^\/[^\/\n\r]+\//, '_RX_').replace(/^\/\/[^\r\n]*/, '//_COMMENT_'); } if (/\/\*/.test(s) || // C-style comments, would make everything really tricky /\w\s*(\/\/[\s\S]*)?\[[\s\S]*\w[\s\S]*\]/.test(s)) { // property accessors, risky return s; } if (/['"\/]/.test(s)) { // drop noisy backslashes s = s.replace(/\\{2,}/g, this._reduceBackslashes); // drop escaped quotes s = s.replace(/\\["'\/]/g, " EQ "); var expr; for (;;) { expr = s.replace(/(^[^'"\/]*[;,\+\-=\(\[]\s*)\/[^\/]+\//g, "$1 _RX_ ") .replace(/(^[^'"\/]*)(["']).*?\2/g, "$1 _QS_ "); if (expr == s) break; s = expr; } } // remove c++ style comments return s.replace(/^([^'"`\\]*?)\/\/[^\r\n]*/g, "$1//_COMMENT_"); }, reduceURLs: function(s) { // nested URLs with protocol are parsed as C++ style comments, and since // they're potentially very expensive, we preemptively remove them if possible while (/^[^'"]*?:\/\//.test(s)) { s = s.replace(/:\/\/[^*\s]*/, ':'); } s = s.replace(/:\/\/[^'"*\n]*/g, ':'); return (/\bhttps?:$/.test(s) && !/\bh\W*t\W*t\W*p\W*s?.*=/.test(s)) ? s.replace(/\b(?:[\w.]+=)?https?:$/, '') : s; }, async reduceJSON(s) { const REPL = 'J'; const toStringRx = /^function\s*toString\(\)\s*{\s*\[native code\]\s*\}$/; // optimistic case first, one big JSON block s = s.replace(/[^{"]+=/, "") let m = s.match(/{[^]+}|\[[^]*{[^]*}[^]*\]/); if (!m) return s; // semicolon-separated JSON chunks, like on syndication.twitter.com 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 await this.reduceJSON(s.replace(expr, REPL)); } } catch (e) {} for (;;) { let prev = s; 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); end = s.lastIndexOf("}"); } try { if (!toStringRx.test(JSON.parse(expr).toString)) continue; this.log("Reducing JSON " + expr); s = `${before}${REPL}${after}`; break; } catch (e) {} if (/\btoString\b[\s\S]*:/.test(expr)) { continue; } let qred = this.reduceQuotes(expr); if (/\{(?:\s*(?:(?:\w+:)+\w+)+;\s*)+\}/.test(qred)) { this.log("Reducing pseudo-JSON " + expr); s = `${before}${REPL}${after}`; break; } if (!/[(=.]|[^:\s]\s*\[|:\s*(?:location|document|set(?:Timeout|Interval)|eval|open|show\w*Dialog|alert|confirm|prompt)\b|(?:\]|set)\s*:/.test(qred) && this.checkJSSyntax("JSON = " + qred) // no-assignment JSON fails with "invalid label" ) { this.log("Reducing slow JSON " + expr); s = `${before}${REPL}${after}`; break; } prevExpr = expr; } if (s === prev) break; } return s; }, reduceXML: function reduceXML(s) { var res; for (let pos = s.indexOf("<"); pos !== -1; pos = s.indexOf("<", 1)) { let head = s.substring(0, pos); let tail = s.substring(pos); let qnum = 0; for (pos = -1; (pos = head.indexOf('"', ++pos)) > -1;) { if (pos === 0 || head[pos - 1] != '\\') qnum++; } if (qnum % 2) break; // odd quotes let t = tail.replace(/^<(\??\s*\/?[a-zA-Z][\w:-]*)(?:[\s+]+[\w:-]+="[^"]*")*[\s+]*(\/?\??)>/, '<$1$2>'); (res || (res = [])).push(head); s = t; } if (res) { res.push(s); s = res.join(''); } return s; }, _singleAssignmentRx: new RegExp( "(?:\\b" + fuzzify('document') + "\\b[^]*\\.|\\s" + fuzzify('setter') + "\\b[^]*=)|/.*/[^]*(?:\\.(?:" + "\\b" + fuzzify("onerror") + "\\b[^]*=|" + +fuzzify('source|toString') + ")|\\[)|" + IC_EVENT_DOS_PATTERN ), _riskyAssignmentRx: new RegExp( "\\b(?:" + fuzzify('location|innerHTML|outerHTML') + ")\\b[^]*=" ), _nameRx: new RegExp( "=[^]*\\b" + fuzzify('name') + "\\b|" + fuzzify("hostname") + "[^]*=[^]*(?:\\b\\d|[\"'{}~^|<*/+-])" ), _evalAliasingRx: new RegExp( "=[^]+\\[" + IC_EVAL_PATTERN + "\\W*\\]" // TODO: check if it can be coalesced into _maybeJSRx ), _maybeJSRx: new RegExp( '(?:(?:\\[[^]+\\]|\\.\\D)(?:[^]*\\([^]*\\)|[^*]`[^]+`|[^=]*=[^=][^]*\\S)' + // double function call '|\\([^]*\\([^]*\\)' + ')|(?:^|\\W)(?:' + IC_EVAL_PATTERN + ')(?:\\W+[^]*|)[(`]|(?:[=(]|\\{[^]+:)[^]*(?:' + // calling eval-like functions directly or... IC_EVAL_PATTERN + // ... assigning them to another function possibly called by the victim later ')[^]*[\\n,;:|]|\\b(?:' + fuzzify('setter|location|innerHTML|outerHTML') + // eval-like assignments ')\\b[^]*=|' + '.' + IC_COMMENT_PATTERN + "src" + IC_COMMENT_PATTERN + '=' + IC_EVENT_DOS_PATTERN + "|\\b" + fuzzify("onerror") + "\\b[^]*=" + "|=[s\\\\[ux]?\d{2}" + // escape (unicode/ascii/octal) "|\\b(?:toString|valueOf)\\b" + IC_COMMENT_PATTERN + "=[^]*(?:" + IC_EVAL_PATTERN + ")" + "|(?:\\)|(?:[^\\w$]|^)[$a-zA-Z_\\u0ff-\\uffff][$\\w\\u0ff-\\uffff]*)" + IC_COMMENT_PATTERN + '=>' + // concise function definition "|(?:[^\\w$]|^)" + IC_EVENT_PATTERN + IC_COMMENT_PATTERN + "=" ), _riskyParensRx: new RegExp( "(?:^|\\W)(?:(?:" + IC_EVAL_PATTERN + "|on\\w+)\\s*[(`]|" + fuzzify("with") + "\\b[^]*\\(|" + fuzzify("for") + "\\b[^]*\\([^]*[\\w$\\u0080-\\uffff]+[^]*\\b(?:" + fuzzify("in|of") + ")\\b)" ), _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) { 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.|]+))$/, _badRightHandRx: /=[\s\S]*(?:_QS_\b|[|.][\s\S]*source\b|<[\s\S]*\/[^>]*>)/, _wikiParensRx: /^(?:[\w.|-]+\/)*\(*[\w\s-]+\([\w\s-]+\)[\w\s-]*\)*$/, _neutralDotsRx: /(?:^|[\/;&#])[\w-]+\.[\w-]+[\?;\&#]/g, _openIdRx: /^scope=(?:\w+\+)\w/, // OpenID authentication scope parameter, see http://forums.informaction.com/viewtopic.php?p=69851#p69851 _gmxRx: /\$\(clientName\)-\$\(dataCenter\)\.(\w+\.)+\w+/, // GMX webmail, see http://forums.informaction.com/viewtopic.php?p=69700#p69700 maybeJS(expr, mavoChecked = false) { if (!mavoChecked && this.maybeMavo(expr)) return true; if (/`[\s\S]*`/.test(expr) || // ES6 templates, extremely insidious!!! this._evalAliasingRx.test(expr) || this._riskyOperatorsRx.test(expr) // this must be checked before removing dots... ) return true; expr = // dotted URL components can lead to false positives, let's remove them expr.replace(this._removeDotsRx, this._removeDots) .replace(this._arrayAccessRx, '_ARRAY_ACCESS_') .replace(/<([\w:]+)>[^/g, '<$1/>') // reduce XML text nodes .replace(/