[XSS] Fallback to execute most demanding regular expressions asynchronously.
This commit is contained in:
parent
96baaa2d51
commit
692803c6f2
2
src/nscl
2
src/nscl
|
@ -1 +1 @@
|
|||
Subproject commit fd083f7200cf9357fc2e907b9609fe8103982c9a
|
||||
Subproject commit 7de366324a204aec9ca3b5a09aa7872683577543
|
|
@ -35,9 +35,9 @@ for (let logType of ["log", "debug", "error"]) {
|
|||
include("InjectionChecker.js");
|
||||
|
||||
{
|
||||
let timingsMap = new Map();
|
||||
const timingsMap = new Map();
|
||||
|
||||
let Handlers = {
|
||||
const Handlers = {
|
||||
async check({xssReq, skip}) {
|
||||
let {destUrl, request, debugging} = xssReq;
|
||||
let {
|
||||
|
@ -72,8 +72,9 @@ include("InjectionChecker.js");
|
|||
Date.now() - xssReq.timestamp, destUrl);
|
||||
}
|
||||
|
||||
postMessage(!(protectName || postInjection || urlInjection) ? null
|
||||
: { protectName, postInjection, urlInjection }
|
||||
postMessage(!(protectName || postInjection || urlInjection)
|
||||
? { xss: false }
|
||||
: { xss: true, protectName, postInjection, urlInjection }
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ XSS.InjectionChecker = (async () => {
|
|||
await include([
|
||||
"/nscl/common/SyntaxChecker.js",
|
||||
"/nscl/common/Base64.js",
|
||||
"/nscl/common/AsyncRegExp.js",
|
||||
"/nscl/common/DebuggableRegExp.js",
|
||||
"/nscl/common/Timing.js",
|
||||
"/xss/ASPIdiocy.js",
|
||||
|
@ -89,9 +90,17 @@ XSS.InjectionChecker = (async () => {
|
|||
},
|
||||
set debugging(b) {
|
||||
this.logEnabled = b;
|
||||
|
||||
for (const rx of ["_maybeJSRx", "_riskyOperatorsRx"]) {
|
||||
if (this[rx].originalRx) this[rx] = this[rx].originalRx;
|
||||
if (b) this[rx] = new DebuggableRegExp(this[rx]);
|
||||
if (b) {
|
||||
this[rx] = new DebuggableRegExp(this[rx], rx => {
|
||||
rx = new AsyncRegExp(rx);
|
||||
// uncomment the following to unconditionally offload to the shared worker
|
||||
// rx.forceRemote = true;
|
||||
return rx;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -123,20 +132,20 @@ XSS.InjectionChecker = (async () => {
|
|||
return false;
|
||||
},
|
||||
|
||||
checkTemplates(script) {
|
||||
async checkTemplates(script) {
|
||||
let templateExpressions = script.replace(/[[\]{}]/g, ";");
|
||||
return templateExpressions !== script &&
|
||||
(this.maybeMavo(script) ||
|
||||
(this.maybeJS(templateExpressions, true) &&
|
||||
(await this.maybeMavo(script) ||
|
||||
(await this.maybeJS(templateExpressions, true) &&
|
||||
(this.syntax.check(templateExpressions) ||
|
||||
/[^><=]=[^=]/.test(templateExpressions) && this.syntax.check(
|
||||
templateExpressions.replace(/([^><=])=(?=[^=])/g, '$1=='))
|
||||
)));
|
||||
},
|
||||
|
||||
maybeMavo(s) {
|
||||
async 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);
|
||||
await 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
|
||||
|
@ -325,7 +334,7 @@ XSS.InjectionChecker = (async () => {
|
|||
"=[^]+\\[" + IC_EVAL_PATTERN + "\\W*\\]" // TODO: check if it can be coalesced into _maybeJSRx
|
||||
),
|
||||
|
||||
_maybeJSRx: new RegExp(
|
||||
_maybeJSRx: new AsyncRegExp(
|
||||
'(?:(?:\\[[^]+\\]|\\.\\D)[^;&/\'"]*(?:/[^]*|)' +
|
||||
'(?:\\([^]*\\)|[^]*`[^]+`|=[^=][^]*\\S)' +
|
||||
// double function call
|
||||
|
@ -358,7 +367,9 @@ XSS.InjectionChecker = (async () => {
|
|||
_arrayAccessRx: /\s*\[\d+\]/g,
|
||||
|
||||
// inc/dec/self-modifying assignments on DOM props or special properties in object literals via Symbol
|
||||
_riskyOperatorsRx: /(?:\+\+|--)\s*(?:\/[*/][\s\S]+)?(?:(?:\$|\w{3,})(?:\/[*/][\s\S]+)?(?:\[|\.\D)|location)|(?:\]|(?:\$|\w{3,})(?:\/[*/][\s\S]+)?\.[^]+|location)\s*(?:\/[*/][\s\S]+)?(\+\+|--|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)|\{[^]*\[[^]*Symbol[^]*(?:\.\D|\[)[^]*:/,
|
||||
_riskyOperatorsRx: new AsyncRegExp(
|
||||
/(?:\+\+|--)\s*(?:\/[*/][\s\S]+)?(?:(?:\$|\w{3,})(?:\/[*/][\s\S]+)?(?:\[|\.\D)|location)|(?:\]|(?:\$|\w{3,})(?:\/[*/][\s\S]+)?\.[^]+|location)\s*(?:\/[*/][\s\S]+)?(\+\+|--|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)|\{[^]*\[[^]*Symbol[^]*(?:\.\D|\[)[^]*:/
|
||||
),
|
||||
|
||||
_assignmentRx: /^(?:[^()="'\s]+=(?:[^(='"\[+]+|[?a-zA-Z_0-9;,&=/]+|[\d.|]+))$/,
|
||||
_badRightHandRx: /=[\s\S]*(?:_QS_\b|[|.][\s\S]*source\b|<[\s\S]*\/[^>]*>)/,
|
||||
|
@ -367,12 +378,12 @@ XSS.InjectionChecker = (async () => {
|
|||
_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;
|
||||
async maybeJS(expr, mavoChecked = false) {
|
||||
if (!mavoChecked && await 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...
|
||||
await this._riskyOperatorsRx.asyncTest(expr) // this must be checked before removing dots...
|
||||
) return true;
|
||||
|
||||
expr = // dotted URL components can lead to false positives, let's remove them
|
||||
|
@ -390,13 +401,13 @@ XSS.InjectionChecker = (async () => {
|
|||
return this._singleAssignmentRx.test(expr) || this._riskyAssignmentRx.test(expr) && this._nameRx.test(expr);
|
||||
|
||||
return this._riskyParensRx.test(expr) ||
|
||||
this._maybeJSRx.test(expr.replace(this._neutralDotsOrParensRx, '')) &&
|
||||
await this._maybeJSRx.asyncTest(expr.replace(this._neutralDotsOrParensRx, '')) &&
|
||||
!this._wikiParensRx.test(expr);
|
||||
|
||||
},
|
||||
|
||||
checkNonTrivialJSSyntax: function(expr) {
|
||||
return this.maybeJS(this.reduceQuotes(expr)) && this.checkJSSyntax(expr);
|
||||
async checkNonTrivialJSSyntax(expr) {
|
||||
return await this.maybeJS(this.reduceQuotes(expr)) && this.checkJSSyntax(expr);
|
||||
},
|
||||
|
||||
|
||||
|
@ -490,14 +501,14 @@ XSS.InjectionChecker = (async () => {
|
|||
return res.join('');
|
||||
},
|
||||
|
||||
checkLastFunction: function() {
|
||||
async checkLastFunction() {
|
||||
var fn = this.syntax.lastFunction;
|
||||
if (!fn) return false;
|
||||
var m = fn.toString().match(/\{([\s\S]*)\}/);
|
||||
if (!m) return false;
|
||||
var expr = this.stripLiteralsAndComments(m[1]);
|
||||
let ret = /=[\s\S]*cookie|\b(?:setter|document|location|(?:inn|out)erHTML|\.\W*src)[\s\S]*=|[\w$\u0080-\uffff\)\]]\s*[\[\(]/.test(expr) ||
|
||||
this.maybeJS(expr);
|
||||
await this.maybeJS(expr);
|
||||
if (ret) {
|
||||
this.escalate(`${expr} has been flagged as dangerous JS (${RegExp.lastMatch})`);
|
||||
}
|
||||
|
@ -563,7 +574,7 @@ XSS.InjectionChecker = (async () => {
|
|||
s += ';' + s.match(/\*\/[\s\S]+/);
|
||||
}
|
||||
|
||||
if (!this.maybeJS(s)) return false;
|
||||
if (!await this.maybeJS(s)) return false;
|
||||
|
||||
const MAX_LOOPS = 1200;
|
||||
|
||||
|
@ -601,7 +612,7 @@ XSS.InjectionChecker = (async () => {
|
|||
let breakSeq = m[1];
|
||||
let quote = breakSeq in this.breakStops ? breakSeq : '';
|
||||
|
||||
if (!this.maybeJS(quote ? quote + subj : subj)) {
|
||||
if (!await this.maybeJS(quote ? quote + subj : subj)) {
|
||||
this.log("Fast escape on " + subj, iterations);
|
||||
return false;
|
||||
}
|
||||
|
@ -609,7 +620,7 @@ XSS.InjectionChecker = (async () => {
|
|||
let script = this.reduceURLs(subj);
|
||||
|
||||
if (script.length < subj.length) {
|
||||
if (!this.maybeJS(script)) {
|
||||
if (!await this.maybeJS(script)) {
|
||||
this.log("Skipping to first nested URL in " + subj, iterations);
|
||||
injectionFinderRx.lastIndex += subj.indexOf("://") + 1;
|
||||
continue;
|
||||
|
@ -686,16 +697,16 @@ XSS.InjectionChecker = (async () => {
|
|||
}
|
||||
|
||||
if (quote) {
|
||||
if (this.checkNonTrivialJSSyntax(expr)) {
|
||||
if (await this.checkNonTrivialJSSyntax(expr)) {
|
||||
this.log("Non-trivial JS inside quoted string detected", iterations);
|
||||
return true;
|
||||
}
|
||||
script = this.syntax.unquote(quote + expr, quote);
|
||||
if (script && this.maybeJS(script) &&
|
||||
(this.checkNonTrivialJSSyntax(script) ||
|
||||
/'./.test(script) && this.checkNonTrivialJSSyntax("''" + script + "'") ||
|
||||
/"./.test(script) && this.checkNonTrivialJSSyntax('""' + script + '"')
|
||||
) && this.checkLastFunction()
|
||||
if (script && await this.maybeJS(script) &&
|
||||
(await this.checkNonTrivialJSSyntax(script) ||
|
||||
/'./.test(script) && await this.checkNonTrivialJSSyntax("''" + script + "'") ||
|
||||
/"./.test(script) && await this.checkNonTrivialJSSyntax('""' + script + '"')
|
||||
) && await this.checkLastFunction()
|
||||
) {
|
||||
this.log("JS quote Break Injection detected", iterations);
|
||||
return true;
|
||||
|
@ -715,14 +726,14 @@ XSS.InjectionChecker = (async () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.maybeJS(this.reduceQuotes(script))) {
|
||||
if (await this.maybeJS(this.reduceQuotes(script))) {
|
||||
|
||||
if (this.checkJSSyntax(script) && this.checkLastFunction()) {
|
||||
if (this.checkJSSyntax(script) && await this.checkLastFunction()) {
|
||||
this.log("JS Break Injection detected", iterations);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.checkTemplates(script)) {
|
||||
if (await this.checkTemplates(script)) {
|
||||
this.log("JS template expression injection detected", iterations);
|
||||
return true;
|
||||
}
|
||||
|
@ -822,7 +833,7 @@ XSS.InjectionChecker = (async () => {
|
|||
|
||||
this.syntax.lastFunction = null;
|
||||
let ret = await this.checkAttributes(s) ||
|
||||
(/[\\\(]|=[^=]/.test(s) || this._riskyOperatorsRx.test(s)) && await this.checkJSBreak(s) || // MAIN
|
||||
(/[\\\(]|=[^=]/.test(s) || await this._riskyOperatorsRx.asyncTest(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;
|
||||
|
|
|
@ -195,8 +195,11 @@ var XSS = (() => {
|
|||
|
||||
if (onBeforeRequest.hasListener(requestListener)) return;
|
||||
|
||||
await include("/legacy/Legacy.js");
|
||||
await include("/xss/Exceptions.js");
|
||||
await include([
|
||||
"/nscl/common/AsyncRegExp.js",
|
||||
"/legacy/Legacy.js",
|
||||
"/xss/Exceptions.js"
|
||||
]);
|
||||
|
||||
this._userChoices = (await Storage.get("sync", "xssUserChoices")).xssUserChoices || {};
|
||||
|
||||
|
@ -301,10 +304,13 @@ var XSS = (() => {
|
|||
}
|
||||
|
||||
let skip = this.Exceptions.partial(xssReq);
|
||||
|
||||
let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js"));
|
||||
|
||||
let {requestId} = xssReq.request;
|
||||
workersMap.set(requestId, worker)
|
||||
return await new Promise((resolve, reject) => {
|
||||
workersMap.set(requestId, worker);
|
||||
AsyncRegExp.connectWorker(worker);
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.onmessage = e => {
|
||||
let {data} = e;
|
||||
if (data) {
|
||||
|
@ -317,9 +323,17 @@ var XSS = (() => {
|
|||
reject(data.error);
|
||||
return;
|
||||
}
|
||||
if (!("xss" in data)) {
|
||||
// someone else's message to handle
|
||||
return;
|
||||
}
|
||||
if (!data.xss) {
|
||||
// let's simplify the returned value
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
cleanup();
|
||||
resolve(e.data);
|
||||
resolve(data);
|
||||
}
|
||||
worker.onerror = worker.onmessageerror = e => {
|
||||
cleanup();
|
||||
|
|
Loading…
Reference in New Issue