type) {
@@ -122,7 +128,7 @@ public void setEnvironment(String environment) {
if (!StringUtils.isBlank(environment))
this.environment = environment;
else
- this.environment = DEFAULTENV;
+ this.environment = DEFAULT_ENV;
}
public String getSnapshotLocation() {
@@ -149,6 +155,17 @@ public void setRetryAfter(String retryAfter) {
this.retryAfter = retryAfter;
}
+ public String getRegexTimeout() {
+ return regexTimeout;
+ }
+
+ public void setRegexTimeout(String regexTimeout) {
+ if (!StringUtils.isBlank(regexTimeout)) {
+ this.regexTimeout = regexTimeout;
+ } else
+ this.regexTimeout = DEFAULT_REGEX_TIMEOUT;
+ }
+
public boolean isSnapshotAutoLoad() {
return snapshotAutoLoad;
}
diff --git a/src/main/java/com/github/switcherapi/client/exception/SwitcherValidatorException.java b/src/main/java/com/github/switcherapi/client/exception/SwitcherValidatorException.java
new file mode 100644
index 00000000..9bc28e6b
--- /dev/null
+++ b/src/main/java/com/github/switcherapi/client/exception/SwitcherValidatorException.java
@@ -0,0 +1,13 @@
+package com.github.switcherapi.client.exception;
+
+/**
+ * @author Roger Floriano (petruki)
+ * @since 2023-02-17
+ */
+public class SwitcherValidatorException extends SwitcherException {
+
+ public SwitcherValidatorException(final String input, final String value) {
+ super(String.format("Failed to process input [%s] for [%s]", input, value), null);
+ }
+
+}
diff --git a/src/main/java/com/github/switcherapi/client/model/ContextKey.java b/src/main/java/com/github/switcherapi/client/model/ContextKey.java
index fde3b0c6..aca2d8ac 100644
--- a/src/main/java/com/github/switcherapi/client/model/ContextKey.java
+++ b/src/main/java/com/github/switcherapi/client/model/ContextKey.java
@@ -72,7 +72,12 @@ public enum ContextKey {
/**
* (boolean) Defines if client will work offline.
*/
- OFFLINE_MODE("switcher.offline", "offlineMode");
+ OFFLINE_MODE("switcher.offline", "offlineMode"),
+
+ /**
+ * (Number) Defines the Timed Match regex time out.
+ */
+ REGEX_TIMEOUT("switcher.regextimeout", "regexTimeout");
private final String param;
private final String propField;
diff --git a/src/main/java/com/github/switcherapi/client/service/ValidatorService.java b/src/main/java/com/github/switcherapi/client/service/ValidatorService.java
index 2422f9a2..d676a2ad 100644
--- a/src/main/java/com/github/switcherapi/client/service/ValidatorService.java
+++ b/src/main/java/com/github/switcherapi/client/service/ValidatorService.java
@@ -24,9 +24,9 @@ private void initializeValidators() {
registerValidator(NetworkValidator.class);
registerValidator(NumericValidator.class);
registerValidator(PayloadValidator.class);
- registerValidator(RegexValidator.class);
registerValidator(TimeValidator.class);
registerValidator(ValueValidator.class);
+ registerValidator(RegexValidatorV8.getPlatformValidator());
}
private StrategyValidator getStrategyValidator(Class extends Validator> validatorClass) {
diff --git a/src/main/java/com/github/switcherapi/client/service/validators/RegexValidator.java b/src/main/java/com/github/switcherapi/client/service/validators/RegexValidator.java
index 68380e38..481394a2 100644
--- a/src/main/java/com/github/switcherapi/client/service/validators/RegexValidator.java
+++ b/src/main/java/com/github/switcherapi/client/service/validators/RegexValidator.java
@@ -1,12 +1,12 @@
package com.github.switcherapi.client.service.validators;
-import java.util.Arrays;
-
import com.github.switcherapi.client.exception.SwitcherInvalidOperationException;
import com.github.switcherapi.client.model.Entry;
import com.github.switcherapi.client.model.StrategyValidator;
import com.github.switcherapi.client.model.criteria.Strategy;
+import java.util.Arrays;
+
@ValidatorComponent(type = StrategyValidator.REGEX)
public class RegexValidator extends Validator {
@@ -15,19 +15,19 @@ public class RegexValidator extends Validator {
@Override
public boolean process(Strategy strategy, Entry switcherInput) throws SwitcherInvalidOperationException {
switch (strategy.getEntryOperation()) {
- case EXIST:
- return Arrays.stream(strategy.getValues()).anyMatch(val -> switcherInput.getInput().matches(val));
- case NOT_EXIST:
- return Arrays.stream(strategy.getValues()).noneMatch(val -> switcherInput.getInput().matches(val));
- case EQUAL:
- return strategy.getValues().length == 1
- && switcherInput.getInput().matches(String.format(DELIMITER_REGEX, strategy.getValues()[0]));
- case NOT_EQUAL:
- return strategy.getValues().length == 1
- && !switcherInput.getInput().matches(String.format(DELIMITER_REGEX, strategy.getValues()[0]));
- default:
- throw new SwitcherInvalidOperationException(strategy.getOperation(), strategy.getStrategy());
+ case EXIST:
+ return Arrays.stream(strategy.getValues()).anyMatch(val -> switcherInput.getInput().matches(val));
+ case NOT_EXIST:
+ return Arrays.stream(strategy.getValues()).noneMatch(val -> switcherInput.getInput().matches(val));
+ case EQUAL:
+ return strategy.getValues().length == 1
+ && switcherInput.getInput().matches(String.format(DELIMITER_REGEX, strategy.getValues()[0]));
+ case NOT_EQUAL:
+ return strategy.getValues().length == 1
+ && !switcherInput.getInput().matches(String.format(DELIMITER_REGEX, strategy.getValues()[0]));
+ default:
+ throw new SwitcherInvalidOperationException(strategy.getOperation(), strategy.getStrategy());
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java b/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java
new file mode 100644
index 00000000..8b8cb8ea
--- /dev/null
+++ b/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java
@@ -0,0 +1,132 @@
+package com.github.switcherapi.client.service.validators;
+
+import com.github.switcherapi.client.SwitcherContextBase;
+import com.github.switcherapi.client.exception.SwitcherInvalidOperationException;
+import com.github.switcherapi.client.exception.SwitcherValidatorException;
+import com.github.switcherapi.client.model.ContextKey;
+import com.github.switcherapi.client.model.Entry;
+import com.github.switcherapi.client.model.StrategyValidator;
+import com.github.switcherapi.client.model.criteria.Strategy;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.*;
+
+/**
+ * Regex Validator for applications running using Java 1.8
+ * 'String.matches' has been improved since Java 9, and it is expected be ReDoS-safe
+ *
+ * This implementation allow you to define the sweet-spot process timing when using Regex Strategies
+ *
+ * @author Roger Floriano (petruki)
+ * @since 2023-02-18
+ */
+@ValidatorComponent(type = StrategyValidator.REGEX)
+public class RegexValidatorV8 extends Validator {
+
+ private static final Logger logger = LogManager.getLogger(RegexValidatorV8.class);
+
+ private static final String DELIMITER_REGEX = "\\b%s\\b";
+ private final Set> blackList;
+ private final TimedMatch timedMatch;
+
+ public RegexValidatorV8() {
+ timedMatch = new TimedMatch();
+ blackList = new HashSet<>();
+ }
+
+ public static Class extends Validator> getPlatformValidator() {
+ if (System.getProperty("java.version").startsWith("1.8"))
+ return RegexValidatorV8.class;
+ return RegexValidator.class;
+ }
+
+ @Override
+ public boolean process(Strategy strategy, Entry switcherInput) throws SwitcherInvalidOperationException {
+ try {
+ switch (strategy.getEntryOperation()) {
+ case EXIST:
+ return Arrays.stream(strategy.getValues()).anyMatch(val -> {
+ try {
+ return timedMatch(switcherInput.getInput(), val);
+ } catch (TimeoutException | SwitcherValidatorException e) {
+ logger.error(e);
+ return false;
+ }
+ });
+ case NOT_EXIST:
+ return Arrays.stream(strategy.getValues()).noneMatch(val -> {
+ try {
+ return timedMatch(switcherInput.getInput(), val);
+ } catch (TimeoutException | SwitcherValidatorException e) {
+ logger.error(e);
+ return true;
+ }
+ });
+ case EQUAL:
+ return strategy.getValues().length == 1
+ && timedMatch(switcherInput.getInput(), String.format(DELIMITER_REGEX, strategy.getValues()[0]));
+ case NOT_EQUAL:
+ return strategy.getValues().length == 1
+ && !timedMatch(switcherInput.getInput(), String.format(DELIMITER_REGEX, strategy.getValues()[0]));
+ default:
+ throw new SwitcherInvalidOperationException(strategy.getOperation(), strategy.getStrategy());
+ }
+ } catch (TimeoutException | SwitcherValidatorException e) {
+ logger.error(e);
+ return false;
+ }
+ }
+
+ private boolean timedMatch(final String input, final String regex) throws TimeoutException {
+ if (isBlackListed(input, regex))
+ throw new SwitcherValidatorException(input, regex);
+
+ timedMatch.init(input, regex);
+ final ExecutorService executor = Executors.newSingleThreadExecutor();
+ final Future future = executor.submit(timedMatch);
+
+ try {
+ return future.get(Integer.parseInt(SwitcherContextBase.contextStr(ContextKey.REGEX_TIMEOUT)),
+ TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ addBlackList(input, regex);
+ future.cancel(true);
+ throw new TimeoutException();
+ } catch (Exception e) {
+ Thread.currentThread().interrupt();
+ throw new SwitcherValidatorException(input, regex);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ private boolean isBlackListed(final String input, final String regex) {
+ return blackList.stream().anyMatch(bl ->
+ regex.equals(bl.getRight()) && bl.getLeft().toLowerCase().contains(input.toLowerCase()));
+ }
+
+ private void addBlackList(final String input, final String regex) {
+ blackList.add(Pair.of(input, regex));
+ }
+
+ static final class TimedMatch implements Callable {
+ private String input;
+ private String regex;
+
+ public void init(String input, String regex) {
+ this.input = input;
+ this.regex = regex;
+ }
+
+ @Override
+ public Boolean call() {
+ return input.matches(regex);
+ }
+ }
+
+}
diff --git a/src/test/java/com/github/switcherapi/Switchers.java b/src/test/java/com/github/switcherapi/Switchers.java
index a0927b23..55a671ca 100644
--- a/src/test/java/com/github/switcherapi/Switchers.java
+++ b/src/test/java/com/github/switcherapi/Switchers.java
@@ -119,6 +119,9 @@ public class Switchers extends SwitcherContext {
@SwitcherKey
public static final String USECASE94 = "USECASE94";
+ @SwitcherKey
+ public static final String USECASE95 = "USECASE95";
+
@SwitcherKey
public static final String USECASE100 = "USECASE100";
diff --git a/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderDefaultsTest.java b/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderDefaultsTest.java
new file mode 100644
index 00000000..1bf240b3
--- /dev/null
+++ b/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderDefaultsTest.java
@@ -0,0 +1,29 @@
+package com.github.switcherapi.client;
+
+import com.github.switcherapi.SwitchersBase;
+import com.github.switcherapi.client.model.ContextKey;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SwitcherContextBuilderDefaultsTest {
+
+ @Test
+ void shouldLoadDefault_environment() {
+ //given
+ SwitchersBase.configure(ContextBuilder.builder(true).environment(null));
+
+ //test
+ assertEquals(SwitcherProperties.DEFAULT_ENV, SwitchersBase.contextStr(ContextKey.ENVIRONMENT));
+ }
+
+ @Test
+ void shouldLoadDefault_regexTimeout() {
+ //given
+ SwitchersBase.configure(ContextBuilder.builder(true).regexTimeout(""));
+
+ //test
+ assertEquals(SwitcherProperties.DEFAULT_REGEX_TIMEOUT, SwitchersBase.contextStr(ContextKey.REGEX_TIMEOUT));
+ }
+
+}
diff --git a/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderTest.java b/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderTest.java
index 327e7f23..8096b861 100644
--- a/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderTest.java
+++ b/src/test/java/com/github/switcherapi/client/SwitcherContextBuilderTest.java
@@ -2,8 +2,7 @@
import static com.github.switcherapi.SwitchersBase.USECASE11;
import static com.github.switcherapi.client.SwitcherContextBase.getSwitcher;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.Paths;
diff --git a/src/test/java/com/github/switcherapi/client/SwitcherContextTest.java b/src/test/java/com/github/switcherapi/client/SwitcherContextTest.java
index 9ca306a4..e0f14cd8 100644
--- a/src/test/java/com/github/switcherapi/client/SwitcherContextTest.java
+++ b/src/test/java/com/github/switcherapi/client/SwitcherContextTest.java
@@ -130,4 +130,17 @@ void shouldThrowError_cannotInstantiateContext() {
assertEquals("Context class cannot be instantiated", ex.getMessage());
}
+ @Test
+ void shouldThrowError_invalidRegexTimeoutFormat() {
+ Switchers.configure(ContextBuilder.builder()
+ .regexTimeout("a"));
+
+ Exception ex = assertThrows(SwitcherContextException.class,
+ Switchers::initializeClient);
+
+ assertEquals(String.format(
+ CONTEXT_ERROR, "Invalid parameter format for [switcher.regextimeout]. Expected class java.lang.Integer."),
+ ex.getMessage());
+ }
+
}
diff --git a/src/test/java/com/github/switcherapi/client/SwitcherOfflineFix1Test.java b/src/test/java/com/github/switcherapi/client/SwitcherOfflineFix1Test.java
index 3cae4700..eb92f7ad 100644
--- a/src/test/java/com/github/switcherapi/client/SwitcherOfflineFix1Test.java
+++ b/src/test/java/com/github/switcherapi/client/SwitcherOfflineFix1Test.java
@@ -1,20 +1,5 @@
package com.github.switcherapi.client;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.nio.file.Paths;
-import java.util.stream.Stream;
-
-import org.apache.commons.lang3.StringUtils;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
import com.github.switcherapi.Switchers;
import com.github.switcherapi.client.exception.SwitcherInvalidNumericFormat;
import com.github.switcherapi.client.exception.SwitcherInvalidTimeFormat;
@@ -25,6 +10,20 @@
import com.github.switcherapi.client.model.Switcher;
import com.github.switcherapi.fixture.Product;
import com.google.gson.Gson;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
class SwitcherOfflineFix1Test {
@@ -305,7 +304,9 @@ static Stream regexTestArguments() {
Arguments.of(Switchers.USECASE93, "user-10", Boolean.FALSE),
//NOT_EQUAL
Arguments.of(Switchers.USECASE94, "user-10", Boolean.TRUE),
- Arguments.of(Switchers.USECASE94, "USER_10", Boolean.FALSE)
+ Arguments.of(Switchers.USECASE94, "USER_10", Boolean.FALSE),
+ //EXIST (ReDoS)
+ Arguments.of(Switchers.USECASE95, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Boolean.FALSE)
);
}
@@ -318,6 +319,19 @@ void offlineShouldTest_regexValidation(String useCaseKey, String input, boolean
switcher.prepareEntry(entry);
assertEquals(expected, switcher.isItOn());
}
+
+ @Test
+ @EnabledOnJre(value = { JRE.JAVA_8 })
+ void offlineShouldTest_evilRegexTimeout() {
+ Switchers.configure(ContextBuilder.builder().regexTimeout("500"));
+
+ Switcher switcher = Switchers.getSwitcher(Switchers.USECASE95);
+ Entry entry = Entry.build(StrategyValidator.REGEX, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+ switcher.prepareEntry(entry);
+ boolean result = assertTimeoutPreemptively(Duration.ofMillis(600), () -> switcher.isItOn());
+ assertFalse(result);
+ }
@ParameterizedTest()
@MethodSource("regexTestArguments")
diff --git a/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java b/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java
new file mode 100644
index 00000000..4fcb36b1
--- /dev/null
+++ b/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java
@@ -0,0 +1,102 @@
+package com.github.switcherapi.client.validator;
+
+import com.github.switcherapi.client.model.Entry;
+import com.github.switcherapi.client.model.EntryOperation;
+import com.github.switcherapi.client.model.StrategyValidator;
+import com.github.switcherapi.client.model.criteria.Strategy;
+import com.github.switcherapi.client.service.validators.RegexValidatorV8;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@EnabledOnJre(value = { JRE.JAVA_8 })
+class RegexValidatorV8Test {
+
+ private static final String EVIL_REGEX = "^(([a-z])+.)+[A-Z]([a-z])+$";
+ private static final String EVIL_INPUT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+
+ static Stream evilRegexTestArguments() {
+ return Stream.of(
+ Arguments.of(EntryOperation.EXIST, Boolean.FALSE),
+ Arguments.of(EntryOperation.NOT_EXIST, Boolean.FALSE),
+ Arguments.of(EntryOperation.EQUAL, Boolean.FALSE),
+ Arguments.of(EntryOperation.NOT_EQUAL, Boolean.FALSE)
+ );
+ }
+
+ @ParameterizedTest()
+ @MethodSource("evilRegexTestArguments")
+ void shouldFailEvilRegexInput(EntryOperation operation, boolean expected) {
+ //given
+ RegexValidatorV8 regexValidator = new RegexValidatorV8();
+ Strategy strategy = givenStrategy(operation, Collections.singletonList(EVIL_REGEX));
+ Entry entry = Entry.build(StrategyValidator.REGEX, EVIL_INPUT);
+
+ //test
+ boolean actual = assertTimeoutPreemptively(Duration.ofMillis(5000), () -> regexValidator.process(strategy, entry));
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void shouldBlackListEvilInput_immediateReturnNextCall() {
+ //given
+ RegexValidatorV8 regexValidator = new RegexValidatorV8();
+ Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX));
+ Entry entry = Entry.build(StrategyValidator.REGEX, EVIL_INPUT);
+
+ //test
+ boolean result = assertTimeoutPreemptively(Duration.ofMillis(4000), () -> regexValidator.process(strategy, entry));
+ assertFalse(result);
+
+ result = assertTimeoutPreemptively(Duration.ofMillis(100), () -> regexValidator.process(strategy, entry));
+ assertFalse(result);
+ }
+
+ @Test
+ void shouldBlackListEvilInput_immediateReturnNextCall_similarInput() {
+ //given
+ RegexValidatorV8 regexValidator = new RegexValidatorV8();
+ Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX));
+
+ //test
+ Entry entry1 = Entry.build(StrategyValidator.REGEX, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ boolean result = assertTimeoutPreemptively(Duration.ofMillis(4000), () -> regexValidator.process(strategy, entry1));
+ assertFalse(result);
+
+ Entry entry2 = Entry.build(StrategyValidator.REGEX, "bbbbaaaaaaaaaaaaaaa");
+ result = assertTimeoutPreemptively(Duration.ofMillis(100), () -> regexValidator.process(strategy, entry2));
+ assertFalse(result);
+ }
+
+ @Test
+ void shouldFail_nullInput() {
+ //given
+ RegexValidatorV8 regexValidator = new RegexValidatorV8();
+ Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX));
+ Entry entry = Entry.build(StrategyValidator.REGEX, null);
+
+ //test
+ boolean result = regexValidator.process(strategy, entry);
+ assertFalse(result);
+ }
+
+ private Strategy givenStrategy(EntryOperation operation, List values) {
+ Strategy strategy = new Strategy();
+ strategy.setStrategy(StrategyValidator.REGEX.toString());
+ strategy.setOperation(operation.toString());
+ strategy.setValues(values.toArray(new String[0]));
+
+ return strategy;
+ }
+
+}
diff --git a/src/test/resources/snapshot_fixture1.json b/src/test/resources/snapshot_fixture1.json
index 42ff5398..6f9a8df3 100644
--- a/src/test/resources/snapshot_fixture1.json
+++ b/src/test/resources/snapshot_fixture1.json
@@ -488,6 +488,22 @@
}
],
"components": ["switcher-client"]
+ },
+ {
+ "key": "USECASE95",
+ "description": "Config with Regex Validation [EXIST] - ReDoS attempt",
+ "activated": true,
+ "strategies": [
+ {
+ "strategy": "REGEX_VALIDATION",
+ "activated": true,
+ "operation": "EXIST",
+ "values": [
+ "^(([a-z])+.)+[A-Z]([a-z])+$"
+ ]
+ }
+ ],
+ "components": ["switcher-client"]
}
]
},