diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index f09f3243..da313230 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,7 +18,7 @@ jobs: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: java-version: 11 distribution: 'temurin' @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v3 - name: Setup Java - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: ${{ matrix.java }} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index f8769649..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Change Log -- 1.3.6: - - Fixed throttle concurrency issue -- 1.3.5: - - Added support to Strategy Payload Validation - - Added event handler for SnapshotWatch - - Added support to external context initiatilization SwitcherContextBase - - Fixed issues with loading snapshot from location/file - - Updated com.google.code.gson:gson from 2.8.6 to 2.9.0 - - Updated Jersey dependencies from 2.35 to 2.36 - - Improved codebase - refactored Constants and API/Local implementation -- 1.3.4: - - Added support to default values for the environment properties file - - Updated org.junit.jupiter:junit-jupiter-api from 5.8.1 to 5.8.2 - - Updated org.apache.logging.log4j:log4j-core from 2.17.0 to 2.17.1 - - Updated managed dependency jackson@2.12.2 to use 2.13.1; fix vulnerability caused by jersey 2.35 -- 1.3.3: - - Updated dependency org.apache.logging.log4j from 2.15.0 to 2.17.0 - - Updated managed dependency junit to 4.13.1 -- 1.3.0: - - Optimized Switcher instance creation management - - Added Throttling and Async calls - - Updated com.google.code.gson:gson from 2.8.6 to 2.8.9 - - Updated Jersey dependencies from 2.34 to 2.35 - - Fixed Autoload snapshot is creating null as file name -- 1.2.1: Medium Severity Security Patch: Jersey has been updated - 2.33 to 2.34 -- 1.2.0: - - Changed how SwitcherContext is implemented - added support to properties file - - Offline mode can programmatically load snapshots - - Added extra security layer for verifying features - - Added @SwitcherMock feature - - Smoke testing - - Removed PowerMockito: tests are way simpler to read using Okhttp3 - - Updated dependecy junit to JUnit5-jupiter -- 1.1.0: - - Improved snapshot lookup mechanism - - Both online and offline modes can validate/update snapshot version -- 1.0.10: - - Dependency patch: Commons Net from 3.7.1 to 3.7.2 - - Critical Fix: Downgraded jersey-media-json-jackson 3.0.0 to 2.33 -- 1.0.9: Security patch - - Updated dependency jersey-client from 2.31 to 2.32 - - Updated dependency jersey-hk2 from 2.31 to 2.32 - - Updated dependency jersey-media-json-jackson from 2.31 to 3.0.0 - - Updated dependency common-net from 3.7 to 3.7.1 -- 1.0.8: - - Fixed issues when using Silent Mode - - Fixed error when using only access to online API - - Improved validation when verifying whether API is accessible - - Added validations when preparing the Switcher Context - - Updated dependency commons-net.version from 3.6 to 3.7 -- 1.0.7: Added Regex Validation -- 1.0.6: Updated depencencies & new features - - Updated dependency jersey-hk2 from 2.28 to 2.31 - - Updated dependency commons-net from 3.3 to 3.6 - - Updated dependency commons-lang3 from 3.8.1 to 3.10 - - Updated dependency gson from 2.8.5 to 2.8.6 - - Added execution log to Switcher - - Added bypass metrics and show detailed criteria evaluation options to Switcher objects -- 1.0.5: Security patch - Jersey has been updated - 2.28 to 2.31 -- 1.0.4: Added Numeric Validation -- 1.0.3: Security patch - Log4J has been updated - 2.13.1 to 2.13.3 -- 1.0.2: - - Improved performance when loading snapshot file. - - Snapshot file auto load when updated. - - Re-worked built-in mock implementation -- 1.0.1: Security patch - Log4J has been updated -- 1.0.0: Working release \ No newline at end of file diff --git a/README.md b/README.md index 7de27e47..a94727e5 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ switcher.snapshot.auto -> true/false Automated lookup for snapshot when loading switcher.snapshot.skipvalidation -> true/false Skip snapshotValidation() that can be used for UT executions switcher.silent -> true/false Contingency in case of some problem with connectivity with the API switcher.retry -> Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours) + +(For applications using Java 1.8 only) +switcher.regextimeout -> Time in ms given to Timed Match used for offline Regex Operation (ReDoS safety mechanism) - 3000 default value ``` ## Client Context Properties - SwitcherContextBase diff --git a/pom.xml b/pom.xml index 033800c3..85cf7ae7 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 1.9.1 - 3.8.1 + 3.10.1 3.2.0 3.0.0-M5 3.0.1 @@ -73,7 +73,8 @@ java **/model/**/*.java, - **/exception/**/*.java + **/exception/**/*.java, + **/service/validators/RegexValidatorV8.java diff --git a/src/main/java/com/github/switcherapi/client/ContextBuilder.java b/src/main/java/com/github/switcherapi/client/ContextBuilder.java index 4f46ae77..e5ca9c51 100644 --- a/src/main/java/com/github/switcherapi/client/ContextBuilder.java +++ b/src/main/java/com/github/switcherapi/client/ContextBuilder.java @@ -75,6 +75,11 @@ public ContextBuilder snapshotFile(String snapshotFile) { return this; } + public ContextBuilder regexTimeout(String regexTimeout) { + properties.setRegexTimeout(regexTimeout); + return this; + } + public ContextBuilder retryAfter(String retryAfter) { properties.setRetryAfter(retryAfter); return this; diff --git a/src/main/java/com/github/switcherapi/client/SwitcherContext.java b/src/main/java/com/github/switcherapi/client/SwitcherContext.java index c019f8ea..6330842b 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherContext.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherContext.java @@ -29,7 +29,7 @@ public abstract class SwitcherContext extends SwitcherContextBase { } /** - * Load properties from the resources folder, look up for resources/switcherapi.properties file. + * Load properties from the resource's folder, look up for resources/switcherapi.properties file. * After loading the properties, it will validate the arguments and load the Switchers in memory. */ public static void loadProperties() { diff --git a/src/main/java/com/github/switcherapi/client/SwitcherContextValidator.java b/src/main/java/com/github/switcherapi/client/SwitcherContextValidator.java index 4763a173..088a351c 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherContextValidator.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherContextValidator.java @@ -2,6 +2,7 @@ import java.io.File; +import com.github.switcherapi.client.model.ContextKey; import org.apache.commons.lang3.StringUtils; import com.github.switcherapi.client.exception.SwitcherContextException; @@ -15,6 +16,7 @@ class SwitcherContextValidator { private static final String SNAPSHOT_PATH_PATTERN = "%s/%s.json"; + private static final String ERR_FORMAT = "Invalid parameter format for [%s]. Expected %s."; private static final String ERR_LOCATION_SNAPSHOT_FILE = "Snapshot locations not defined [add: switcher.snapshot.location or switcher.snapshot.file]"; private static final String ERR_SNAPSHOT_FILE = "Snapshot file not defined [add: switcher.snapshot.file]"; private static final String ERR_LOCATION = "Snapshot location not defined [add: switcher.snapshot.location]"; @@ -97,10 +99,17 @@ public static void validateOptionals(final SwitcherProperties prop) { if (prop.isSnapshotAutoLoad() && StringUtils.isBlank(prop.getSnapshotLocation())) { throw new SwitcherContextException(ERR_LOCATION); } - + if (prop.isSilentMode() && StringUtils.isBlank(prop.getRetryAfter())) { throw new SwitcherContextException(ERR_RETRY); } + + try { + Integer.parseInt(prop.getRegexTimeout()); + } catch (NumberFormatException e) { + throw new SwitcherContextException( + String.format(ERR_FORMAT, ContextKey.REGEX_TIMEOUT.getParam(), Integer.class)); + } } /** diff --git a/src/main/java/com/github/switcherapi/client/SwitcherProperties.java b/src/main/java/com/github/switcherapi/client/SwitcherProperties.java index ba203d51..bfa8271d 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherProperties.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherProperties.java @@ -17,7 +17,9 @@ */ class SwitcherProperties { - public static final String DEFAULTENV = "default"; + public static final String DEFAULT_ENV = "default"; + + public static final String DEFAULT_REGEX_TIMEOUT = "3000"; private String contextLocation; @@ -37,6 +39,8 @@ class SwitcherProperties { private String retryAfter; + private String regexTimeout; + private boolean snapshotAutoLoad; private boolean snapshotSkipValidation; @@ -46,7 +50,8 @@ class SwitcherProperties { private boolean offlineMode; public SwitcherProperties() { - this.environment = DEFAULTENV; + this.environment = DEFAULT_ENV; + this.regexTimeout = DEFAULT_REGEX_TIMEOUT; } public void loadFromProperties(Properties prop) { @@ -63,6 +68,7 @@ public void loadFromProperties(Properties prop) { setSilentMode(Boolean.parseBoolean(SwitcherUtils.resolveProperties(ContextKey.SILENT_MODE.getParam(), prop))); setOfflineMode(Boolean.parseBoolean(SwitcherUtils.resolveProperties(ContextKey.OFFLINE_MODE.getParam(), prop))); setRetryAfter(SwitcherUtils.resolveProperties(ContextKey.RETRY_AFTER.getParam(), prop)); + setRegexTimeout(SwitcherUtils.resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop)); } public T getValue(ContextKey contextKey, Class 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 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 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"] } ] },