Skip to content

Commit 3747525

Browse files
authored
Merge pull request from GHSA-wqxw-8h5g-hq56
Fixes CVE-2023-23925 - Added TimedMatch for safe REGEX execution
2 parents 28933c7 + eb66efd commit 3747525

17 files changed

+401
-166
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ Switcher.buildContext({ url, apiKey, domain, component, environment }, {
7272
let switcher = Switcher.factory();
7373
```
7474

75-
- **offline**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'.
75+
- **offline**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'
7676
- **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Switcher.getLogger('KEY')
77-
- **snapshotLocation**: Location of snapshot files. The default value is './snapshot/'.
78-
- **silentMode**: If activated, all connectivity issues will be ignored and the client will automatically fetch the configuration into your snapshot file.
79-
- **retryAfter** : Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours).
77+
- **snapshotLocation**: Location of snapshot files. The default value is './snapshot/'
78+
- **silentMode**: If activated, all connectivity issues will be ignored and the client will automatically fetch the configuration into your snapshot file
79+
- **retryAfter**: Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours)
80+
- **regexMaxBlackList**: Number of entries cached when REGEX Strategy fails to perform (reDOS safe) - default: 50
81+
- **regexMaxTimeLimit**: Time limit (ms) used by REGEX workers (reDOS safe) - default - 3000ms
8082

8183
## Executing
8284
There are a few different ways to call the API using the JavaScript module.

snapshot/default.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@
7474
}
7575
],
7676
"components": []
77+
},
78+
{
79+
"key": "FF2FOR2024",
80+
"description": "reDOS safe test",
81+
"activated": true,
82+
"strategies": [
83+
{
84+
"strategy": "REGEX_VALIDATION",
85+
"activated": true,
86+
"operation": "EXIST",
87+
"values": [
88+
"^(([a-z])+.)+[A-Z]([a-z])+$"
89+
]
90+
}
91+
],
92+
"components": []
7793
}
7894
]
7995
},

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ sonar.javascript.lcov.reportPaths=coverage/lcov.info
1010
sonar.sources=src
1111
sonar.tests=test
1212
sonar.language=js
13-
sonar.exclusions=src/**/*.d.ts
13+
sonar.exclusions=src/**/*.d.ts, src/lib/utils/timed-match/match-proc.js
1414

1515
sonar.dynamicAnalysis=reuseReports
1616
# Encoding of the source code. Default is default system encoding

src/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ declare namespace SwitcherClient {
140140
snapshotLocation: string;
141141
silentMode: boolean;
142142
retryAfter: string;
143+
regexMaxBlackList: number;
144+
regexMaxTimeLimit: number;
143145
}
144146

145147
/**

src/index.js

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const Bypasser = require('./lib/bypasser');
44
const ExecutionLogger = require('./lib/utils/executionLogger');
5+
const TimedMatch = require('./lib/utils/timed-match');
56
const DateMoment = require('./lib/utils/datemoment');
67
const { loadDomain, validateSnapshot, checkSwitchers } = require('./lib/snapshot');
78
const services = require('./lib/remote');
@@ -74,6 +75,8 @@ class Switcher {
7475
this.options.retryTime = DEFAULT_RETRY_TIME.charAt(0);
7576
this.options.retryDurationIn = DEFAULT_RETRY_TIME.charAt(1);
7677
}
78+
79+
this.#initTimedMatch(options);
7780
}
7881
}
7982

@@ -86,7 +89,7 @@ class Switcher {
8689
return false;
8790

8891
if (!Switcher.context.exp || Date.now() > (Switcher.context.exp*1000))
89-
await Switcher._auth();
92+
await Switcher.#auth();
9093

9194
const result = await validateSnapshot(
9295
Switcher.context,
@@ -141,19 +144,30 @@ class Switcher {
141144
if (Switcher.options.offline) {
142145
checkSwitchers(Switcher.snapshot, switcherKeys);
143146
} else {
144-
await Switcher._auth();
147+
await Switcher.#auth();
145148
await services.checkSwitchers(
146149
Switcher.context.url, Switcher.context.token, switcherKeys);
147150
}
148151
}
149152

150-
static async _auth() {
153+
static #initTimedMatch(options) {
154+
if ('regexMaxBlackList' in options) {
155+
TimedMatch.setMaxBlackListed(options.regexMaxBlackList);
156+
}
157+
158+
if ('regexMaxTimeLimit' in options) {
159+
TimedMatch.setMaxTimeLimit(options.regexMaxTimeLimit);
160+
}
161+
}
162+
163+
164+
static async #auth() {
151165
const response = await services.auth(Switcher.context);
152166
Switcher.context.token = response.token;
153167
Switcher.context.exp = response.exp;
154168
}
155169

156-
static async _checkHealth() {
170+
static async #checkHealth() {
157171
// checks if silent mode is still activated
158172
if (Switcher.context.token === 'SILENT') {
159173
if (!Switcher.context.exp || Date.now() < (Switcher.context.exp*1000)) {
@@ -210,7 +224,7 @@ class Switcher {
210224
if (input) { this._input = input; }
211225

212226
if (!Switcher.options.offline) {
213-
await Switcher._auth();
227+
await Switcher.#auth();
214228
}
215229
}
216230

@@ -233,7 +247,7 @@ class Switcher {
233247
errors.push('Missing key field');
234248
}
235249

236-
await this._executeApiValidation();
250+
await this.#executeApiValidation();
237251
if (!Switcher.context.token) {
238252
errors.push('Missing token field');
239253
}
@@ -245,7 +259,7 @@ class Switcher {
245259

246260
async isItOn(key, input, showReason = false) {
247261
let result;
248-
this._validateArgs(key, input);
262+
this.#validateArgs(key, input);
249263

250264
// verify if query from Bypasser
251265
const bypassKey = Bypasser.searchBypassed(this._key);
@@ -255,13 +269,13 @@ class Switcher {
255269

256270
// verify if query from snapshot
257271
if (Switcher.options.offline) {
258-
result = this._executeOfflineCriteria();
272+
result = await this.#executeOfflineCriteria();
259273
} else {
260274
await this.validate();
261275
if (Switcher.context.token === 'SILENT')
262-
result = this._executeOfflineCriteria();
276+
result = await this.#executeOfflineCriteria();
263277
else
264-
result = await this._executeOnlineCriteria(showReason);
278+
result = await this.#executeOnlineCriteria(showReason);
265279
}
266280

267281
return result;
@@ -276,8 +290,8 @@ class Switcher {
276290
return this;
277291
}
278292

279-
async _executeOnlineCriteria(showReason) {
280-
if (!this._useSync())
293+
async #executeOnlineCriteria(showReason) {
294+
if (!this.#useSync())
281295
return this._executeAsyncOnlineCriteria(showReason);
282296

283297
const responseCriteria = await services.checkCriteria(
@@ -299,17 +313,17 @@ class Switcher {
299313
return ExecutionLogger.getExecution(this._key, this._input).response.result;
300314
}
301315

302-
async _executeApiValidation() {
303-
if (this._useSync()) {
304-
if (await Switcher._checkHealth() &&
316+
async #executeApiValidation() {
317+
if (this.#useSync()) {
318+
if (await Switcher.#checkHealth() &&
305319
(!Switcher.context.exp || Date.now() > (Switcher.context.exp * 1000))) {
306320
await this.prepare(this._key, this._input);
307321
}
308322
}
309323
}
310324

311-
_executeOfflineCriteria() {
312-
const response = checkCriteriaOffline(
325+
async #executeOfflineCriteria() {
326+
const response = await checkCriteriaOffline(
313327
this._key, this._input, Switcher.snapshot);
314328

315329
if (Switcher.options.logger)
@@ -318,12 +332,12 @@ class Switcher {
318332
return response.result;
319333
}
320334

321-
_validateArgs(key, input) {
335+
#validateArgs(key, input) {
322336
if (key) { this._key = key; }
323337
if (input) { this._input = input; }
324338
}
325339

326-
_useSync() {
340+
#useSync() {
327341
return this._delay == 0 || !ExecutionLogger.getExecution(this._key, this._input);
328342
}
329343

src/lib/resolver.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { processOperation } = require('./snapshot');
22
const services = require('../lib/remote');
33

4-
function resolveCriteria(key, input, { domain }) {
4+
async function resolveCriteria(key, input, { domain }) {
55
let result = true, reason = '';
66

77
try {
@@ -10,7 +10,7 @@ function resolveCriteria(key, input, { domain }) {
1010
}
1111

1212
const { group } = domain;
13-
if (!checkGroup(group, key, input)) {
13+
if (!(await checkGroup(group, key, input))) {
1414
throw new Error(`Something went wrong: {"error":"Unable to load a key ${key}"}`);
1515
}
1616

@@ -36,14 +36,14 @@ function resolveCriteria(key, input, { domain }) {
3636
* @param {*} input strategy if exists
3737
* @return true if Switcher found
3838
*/
39-
function checkGroup(groups, key, input) {
39+
async function checkGroup(groups, key, input) {
4040
if (groups) {
4141
for (const group of groups) {
4242
const { config } = group;
4343
const configFound = config.filter(c => c.key === key);
4444

4545
// Switcher Configs are always supplied as the snapshot is loaded from components linked to the Switcher.
46-
if (checkConfig(group, configFound[0], input)) {
46+
if (await checkConfig(group, configFound[0], input)) {
4747
return true;
4848
}
4949
}
@@ -57,7 +57,7 @@ function checkGroup(groups, key, input) {
5757
* @param {*} input Strategy input if exists
5858
* @return true if Switcher found
5959
*/
60-
function checkConfig(group, config, input) {
60+
async function checkConfig(group, config, input) {
6161
if (!config)
6262
return false;
6363

@@ -70,44 +70,44 @@ function checkConfig(group, config, input) {
7070
}
7171

7272
if (config.strategies) {
73-
return checkStrategy(config, input);
73+
return await checkStrategy(config, input);
7474
}
7575

7676
return true;
7777
}
7878

79-
function checkStrategy(config, input) {
79+
async function checkStrategy(config, input) {
8080
const { strategies } = config;
8181
const entry = services.getEntry(input);
8282

8383
for (const strategy of strategies) {
8484
if (!strategy.activated)
8585
continue;
8686

87-
checkStrategyInput(entry, strategy);
87+
await checkStrategyInput(entry, strategy);
8888
}
8989

9090
return true;
9191
}
9292

93-
function checkStrategyInput(entry, { strategy, operation, values }) {
93+
async function checkStrategyInput(entry, { strategy, operation, values }) {
9494
if (entry && entry.length) {
9595
const strategyEntry = entry.filter(e => e.strategy === strategy);
96-
if (strategyEntry.length == 0 || !processOperation(strategy, operation, strategyEntry[0].input, values)) {
96+
if (strategyEntry.length == 0 || !(await processOperation(strategy, operation, strategyEntry[0].input, values))) {
9797
throw new CriteriaFailed(`Strategy '${strategy}' does not agree`);
9898
}
9999
} else {
100100
throw new CriteriaFailed(`Strategy '${strategy}' did not receive any input`);
101101
}
102102
}
103103

104-
function checkCriteriaOffline(key, input, snapshot) {
104+
async function checkCriteriaOffline(key, input, snapshot) {
105105
if (!snapshot) {
106106
throw new Error('Snapshot not loaded. Try to use \'Switcher.loadSnapshot()\'');
107107
}
108108

109109
const { data } = snapshot;
110-
return resolveCriteria(key, input, data);
110+
return await resolveCriteria(key, input, data);
111111
}
112112

113113
class CriteriaFailed extends Error {

src/lib/snapshot.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const fs = require('fs');
22
const IPCIDR = require('./utils/ipcidr');
33
const DateMoment = require('./utils/datemoment');
4+
const TimedMatch = require('./utils/timed-match');
45
const { resolveSnapshot, checkSnapshotVersion } = require('./remote');
56
const { CheckSwitcherError } = require('./exceptions');
67
const { parseJSON, payloadReader } = require('./utils/payloadReader');
@@ -83,7 +84,7 @@ const OperationsType = Object.freeze({
8384
HAS_ALL: 'HAS_ALL'
8485
});
8586

86-
const processOperation = (strategy, operation, input, values) => {
87+
const processOperation = async (strategy, operation, input, values) => {
8788
switch(strategy) {
8889
case StrategiesType.NETWORK:
8990
return processNETWORK(operation, input, values);
@@ -202,22 +203,16 @@ function processDATE(operation, input, values) {
202203
}
203204
}
204205

205-
function processREGEX(operation, input, values) {
206+
async function processREGEX(operation, input, values) {
206207
switch(operation) {
207-
case OperationsType.EXIST: {
208-
for (const value of values) {
209-
if (input.match(value)) {
210-
return true;
211-
}
212-
}
213-
return false;
214-
}
208+
case OperationsType.EXIST:
209+
return await TimedMatch.tryMatch(values, input);
215210
case OperationsType.NOT_EXIST:
216-
return !processREGEX(OperationsType.EXIST, input, values);
211+
return !(await processREGEX(OperationsType.EXIST, input, values));
217212
case OperationsType.EQUAL:
218-
return input.match(`\\b${values[0]}\\b`) != null;
213+
return await TimedMatch.tryMatch([`\\b${values[0]}\\b`], input);
219214
case OperationsType.NOT_EQUAL:
220-
return !processREGEX(OperationsType.EQUAL, input, values);
215+
return !(await TimedMatch.tryMatch([`\\b${values[0]}\\b`], input));
221216
}
222217
}
223218

0 commit comments

Comments
 (0)