Skip to content

Commit 692803c

Browse files
committed
[XSS] Fallback to execute most demanding regular expressions asynchronously.
1 parent 96baaa2 commit 692803c

File tree

4 files changed

+65
-39
lines changed

4 files changed

+65
-39
lines changed

src/nscl

src/xss/InjectionCheckWorker.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ for (let logType of ["log", "debug", "error"]) {
3535
include("InjectionChecker.js");
3636

3737
{
38-
let timingsMap = new Map();
38+
const timingsMap = new Map();
3939

40-
let Handlers = {
40+
const Handlers = {
4141
async check({xssReq, skip}) {
4242
let {destUrl, request, debugging} = xssReq;
4343
let {
@@ -72,8 +72,9 @@ include("InjectionChecker.js");
7272
Date.now() - xssReq.timestamp, destUrl);
7373
}
7474

75-
postMessage(!(protectName || postInjection || urlInjection) ? null
76-
: { protectName, postInjection, urlInjection }
75+
postMessage(!(protectName || postInjection || urlInjection)
76+
? { xss: false }
77+
: { xss: true, protectName, postInjection, urlInjection }
7778
);
7879
},
7980

src/xss/InjectionChecker.js

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ XSS.InjectionChecker = (async () => {
2222
await include([
2323
"/nscl/common/SyntaxChecker.js",
2424
"/nscl/common/Base64.js",
25+
"/nscl/common/AsyncRegExp.js",
2526
"/nscl/common/DebuggableRegExp.js",
2627
"/nscl/common/Timing.js",
2728
"/xss/ASPIdiocy.js",
@@ -89,9 +90,17 @@ XSS.InjectionChecker = (async () => {
8990
},
9091
set debugging(b) {
9192
this.logEnabled = b;
93+
9294
for (const rx of ["_maybeJSRx", "_riskyOperatorsRx"]) {
9395
if (this[rx].originalRx) this[rx] = this[rx].originalRx;
94-
if (b) this[rx] = new DebuggableRegExp(this[rx]);
96+
if (b) {
97+
this[rx] = new DebuggableRegExp(this[rx], rx => {
98+
rx = new AsyncRegExp(rx);
99+
// uncomment the following to unconditionally offload to the shared worker
100+
// rx.forceRemote = true;
101+
return rx;
102+
});
103+
}
95104
}
96105
},
97106

@@ -123,20 +132,20 @@ XSS.InjectionChecker = (async () => {
123132
return false;
124133
},
125134

126-
checkTemplates(script) {
135+
async checkTemplates(script) {
127136
let templateExpressions = script.replace(/[[\]{}]/g, ";");
128137
return templateExpressions !== script &&
129-
(this.maybeMavo(script) ||
130-
(this.maybeJS(templateExpressions, true) &&
138+
(await this.maybeMavo(script) ||
139+
(await this.maybeJS(templateExpressions, true) &&
131140
(this.syntax.check(templateExpressions) ||
132141
/[^><=]=[^=]/.test(templateExpressions) && this.syntax.check(
133142
templateExpressions.replace(/([^><=])=(?=[^=])/g, '$1=='))
134143
)));
135144
},
136145

137-
maybeMavo(s) {
146+
async maybeMavo(s) {
138147
return /\[[^]*\([^]*\)[^]*\]/.test(s) && /\b(?:and|or|mod|\$url\b)/.test(s) &&
139-
this.maybeJS(s.replace(/\b(?:and|or|mod|[[\]])/g, ',').replace(/\$url\b/g, 'location'), true);
148+
await this.maybeJS(s.replace(/\b(?:and|or|mod|[[\]])/g, ',').replace(/\$url\b/g, 'location'), true);
140149
},
141150
get breakStops() {
142151
var def = "\\/\\?&#;\\s\\x00}<>"; // we stop on URL, JS and HTML delimiters
@@ -325,7 +334,7 @@ XSS.InjectionChecker = (async () => {
325334
"=[^]+\\[" + IC_EVAL_PATTERN + "\\W*\\]" // TODO: check if it can be coalesced into _maybeJSRx
326335
),
327336

328-
_maybeJSRx: new RegExp(
337+
_maybeJSRx: new AsyncRegExp(
329338
'(?:(?:\\[[^]+\\]|\\.\\D)[^;&/\'"]*(?:/[^]*|)' +
330339
'(?:\\([^]*\\)|[^]*`[^]+`|=[^=][^]*\\S)' +
331340
// double function call
@@ -358,7 +367,9 @@ XSS.InjectionChecker = (async () => {
358367
_arrayAccessRx: /\s*\[\d+\]/g,
359368

360369
// inc/dec/self-modifying assignments on DOM props or special properties in object literals via Symbol
361-
_riskyOperatorsRx: /(?:\+\+|--)\s*(?:\/[*/][\s\S]+)?(?:(?:\$|\w{3,})(?:\/[*/][\s\S]+)?(?:\[|\.\D)|location)|(?:\]|(?:\$|\w{3,})(?:\/[*/][\s\S]+)?\.[^]+|location)\s*(?:\/[*/][\s\S]+)?(\+\+|--|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)|\{[^]*\[[^]*Symbol[^]*(?:\.\D|\[)[^]*:/,
370+
_riskyOperatorsRx: new AsyncRegExp(
371+
/(?:\+\+|--)\s*(?:\/[*/][\s\S]+)?(?:(?:\$|\w{3,})(?:\/[*/][\s\S]+)?(?:\[|\.\D)|location)|(?:\]|(?:\$|\w{3,})(?:\/[*/][\s\S]+)?\.[^]+|location)\s*(?:\/[*/][\s\S]+)?(\+\+|--|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)|\{[^]*\[[^]*Symbol[^]*(?:\.\D|\[)[^]*:/
372+
),
362373

363374
_assignmentRx: /^(?:[^()="'\s]+=(?:[^(='"\[+]+|[?a-zA-Z_0-9;,&=/]+|[\d.|]+))$/,
364375
_badRightHandRx: /=[\s\S]*(?:_QS_\b|[|.][\s\S]*source\b|<[\s\S]*\/[^>]*>)/,
@@ -367,12 +378,12 @@ XSS.InjectionChecker = (async () => {
367378
_openIdRx: /^scope=(?:\w+\+)\w/, // OpenID authentication scope parameter, see http://forums.informaction.com/viewtopic.php?p=69851#p69851
368379
_gmxRx: /\$\(clientName\)-\$\(dataCenter\)\.(\w+\.)+\w+/, // GMX webmail, see http://forums.informaction.com/viewtopic.php?p=69700#p69700
369380

370-
maybeJS(expr, mavoChecked = false) {
371-
if (!mavoChecked && this.maybeMavo(expr)) return true;
381+
async maybeJS(expr, mavoChecked = false) {
382+
if (!mavoChecked && await this.maybeMavo(expr)) return true;
372383

373384
if (/`[\s\S]*`/.test(expr) || // ES6 templates, extremely insidious!!!
374385
this._evalAliasingRx.test(expr) ||
375-
this._riskyOperatorsRx.test(expr) // this must be checked before removing dots...
386+
await this._riskyOperatorsRx.asyncTest(expr) // this must be checked before removing dots...
376387
) return true;
377388

378389
expr = // dotted URL components can lead to false positives, let's remove them
@@ -390,13 +401,13 @@ XSS.InjectionChecker = (async () => {
390401
return this._singleAssignmentRx.test(expr) || this._riskyAssignmentRx.test(expr) && this._nameRx.test(expr);
391402

392403
return this._riskyParensRx.test(expr) ||
393-
this._maybeJSRx.test(expr.replace(this._neutralDotsOrParensRx, '')) &&
404+
await this._maybeJSRx.asyncTest(expr.replace(this._neutralDotsOrParensRx, '')) &&
394405
!this._wikiParensRx.test(expr);
395406

396407
},
397408

398-
checkNonTrivialJSSyntax: function(expr) {
399-
return this.maybeJS(this.reduceQuotes(expr)) && this.checkJSSyntax(expr);
409+
async checkNonTrivialJSSyntax(expr) {
410+
return await this.maybeJS(this.reduceQuotes(expr)) && this.checkJSSyntax(expr);
400411
},
401412

402413

@@ -490,14 +501,14 @@ XSS.InjectionChecker = (async () => {
490501
return res.join('');
491502
},
492503

493-
checkLastFunction: function() {
504+
async checkLastFunction() {
494505
var fn = this.syntax.lastFunction;
495506
if (!fn) return false;
496507
var m = fn.toString().match(/\{([\s\S]*)\}/);
497508
if (!m) return false;
498509
var expr = this.stripLiteralsAndComments(m[1]);
499510
let ret = /=[\s\S]*cookie|\b(?:setter|document|location|(?:inn|out)erHTML|\.\W*src)[\s\S]*=|[\w$\u0080-\uffff\)\]]\s*[\[\(]/.test(expr) ||
500-
this.maybeJS(expr);
511+
await this.maybeJS(expr);
501512
if (ret) {
502513
this.escalate(`${expr} has been flagged as dangerous JS (${RegExp.lastMatch})`);
503514
}
@@ -563,7 +574,7 @@ XSS.InjectionChecker = (async () => {
563574
s += ';' + s.match(/\*\/[\s\S]+/);
564575
}
565576

566-
if (!this.maybeJS(s)) return false;
577+
if (!await this.maybeJS(s)) return false;
567578

568579
const MAX_LOOPS = 1200;
569580

@@ -601,15 +612,15 @@ XSS.InjectionChecker = (async () => {
601612
let breakSeq = m[1];
602613
let quote = breakSeq in this.breakStops ? breakSeq : '';
603614

604-
if (!this.maybeJS(quote ? quote + subj : subj)) {
615+
if (!await this.maybeJS(quote ? quote + subj : subj)) {
605616
this.log("Fast escape on " + subj, iterations);
606617
return false;
607618
}
608619

609620
let script = this.reduceURLs(subj);
610621

611622
if (script.length < subj.length) {
612-
if (!this.maybeJS(script)) {
623+
if (!await this.maybeJS(script)) {
613624
this.log("Skipping to first nested URL in " + subj, iterations);
614625
injectionFinderRx.lastIndex += subj.indexOf("://") + 1;
615626
continue;
@@ -686,16 +697,16 @@ XSS.InjectionChecker = (async () => {
686697
}
687698

688699
if (quote) {
689-
if (this.checkNonTrivialJSSyntax(expr)) {
700+
if (await this.checkNonTrivialJSSyntax(expr)) {
690701
this.log("Non-trivial JS inside quoted string detected", iterations);
691702
return true;
692703
}
693704
script = this.syntax.unquote(quote + expr, quote);
694-
if (script && this.maybeJS(script) &&
695-
(this.checkNonTrivialJSSyntax(script) ||
696-
/'./.test(script) && this.checkNonTrivialJSSyntax("''" + script + "'") ||
697-
/"./.test(script) && this.checkNonTrivialJSSyntax('""' + script + '"')
698-
) && this.checkLastFunction()
705+
if (script && await this.maybeJS(script) &&
706+
(await this.checkNonTrivialJSSyntax(script) ||
707+
/'./.test(script) && await this.checkNonTrivialJSSyntax("''" + script + "'") ||
708+
/"./.test(script) && await this.checkNonTrivialJSSyntax('""' + script + '"')
709+
) && await this.checkLastFunction()
699710
) {
700711
this.log("JS quote Break Injection detected", iterations);
701712
return true;
@@ -715,14 +726,14 @@ XSS.InjectionChecker = (async () => {
715726
}
716727
}
717728

718-
if (this.maybeJS(this.reduceQuotes(script))) {
729+
if (await this.maybeJS(this.reduceQuotes(script))) {
719730

720-
if (this.checkJSSyntax(script) && this.checkLastFunction()) {
731+
if (this.checkJSSyntax(script) && await this.checkLastFunction()) {
721732
this.log("JS Break Injection detected", iterations);
722733
return true;
723734
}
724735

725-
if (this.checkTemplates(script)) {
736+
if (await this.checkTemplates(script)) {
726737
this.log("JS template expression injection detected", iterations);
727738
return true;
728739
}
@@ -822,7 +833,7 @@ XSS.InjectionChecker = (async () => {
822833

823834
this.syntax.lastFunction = null;
824835
let ret = await this.checkAttributes(s) ||
825-
(/[\\\(]|=[^=]/.test(s) || this._riskyOperatorsRx.test(s)) && await this.checkJSBreak(s) || // MAIN
836+
(/[\\\(]|=[^=]/.test(s) || await this._riskyOperatorsRx.asyncTest(s)) && await this.checkJSBreak(s) || // MAIN
826837
hasUnicodeEscapes && await this.checkJS(this.unescapeJS(s), true); // optional unescaped recursion
827838
if (ret) {
828839
let msg = "JavaScript Injection in " + s;

src/xss/XSS.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,11 @@ var XSS = (() => {
195195

196196
if (onBeforeRequest.hasListener(requestListener)) return;
197197

198-
await include("/legacy/Legacy.js");
199-
await include("/xss/Exceptions.js");
198+
await include([
199+
"/nscl/common/AsyncRegExp.js",
200+
"/legacy/Legacy.js",
201+
"/xss/Exceptions.js"
202+
]);
200203

201204
this._userChoices = (await Storage.get("sync", "xssUserChoices")).xssUserChoices || {};
202205

@@ -301,10 +304,13 @@ var XSS = (() => {
301304
}
302305

303306
let skip = this.Exceptions.partial(xssReq);
307+
304308
let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js"));
309+
305310
let {requestId} = xssReq.request;
306-
workersMap.set(requestId, worker)
307-
return await new Promise((resolve, reject) => {
311+
workersMap.set(requestId, worker);
312+
AsyncRegExp.connectWorker(worker);
313+
return new Promise((resolve, reject) => {
308314
worker.onmessage = e => {
309315
let {data} = e;
310316
if (data) {
@@ -317,9 +323,17 @@ var XSS = (() => {
317323
reject(data.error);
318324
return;
319325
}
326+
if (!("xss" in data)) {
327+
// someone else's message to handle
328+
return;
329+
}
330+
if (!data.xss) {
331+
// let's simplify the returned value
332+
data = null;
333+
}
320334
}
321335
cleanup();
322-
resolve(e.data);
336+
resolve(data);
323337
}
324338
worker.onerror = worker.onmessageerror = e => {
325339
cleanup();

0 commit comments

Comments
 (0)