Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created Fuzzing against Jackson and fixed multiple small bugs found with ti #28

Merged
merged 8 commits into from
Dec 29, 2023
25 changes: 14 additions & 11 deletions src/jmh/java/dev/blaauwendraad/masker/json/BaselineBenchmark.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package dev.blaauwendraad.masker.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.blaauwendraad.masker.json.config.JsonMaskingConfig;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.Warmup;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -22,17 +30,16 @@ public class BaselineBenchmark {

@org.openjdk.jmh.annotations.State(Scope.Thread)
public static class State {
@Param({"1kb", "128kb", "2mb"})
@Param({ "1kb", "128kb", "2mb" })
String jsonSize;
@Param({"unicode"})
@Param({ "unicode" })
String characters;
@Param({"0.01"})
@Param({ "0.01" })
double maskedKeyProbability;

private Set<String> targetKeys;
private String jsonString;
private byte[] jsonBytes;
private ObjectMapper objectMapper;
private List<Pattern> regexList;

@Setup
Expand All @@ -41,8 +48,6 @@ public synchronized void setup() {
jsonString = BenchmarkUtils.randomJson(targetKeys, jsonSize, characters, maskedKeyProbability);
jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);

objectMapper = new ObjectMapper();

regexList = targetKeys.stream()
// will only match primitive values, not objects or arrays, but it's good to show the difference
.map(key -> Pattern.compile("(\"" + key + "\"\\s*:\\s*)(\"?[^\"]*\"?)", Pattern.CASE_INSENSITIVE))
Expand Down Expand Up @@ -80,9 +85,7 @@ public String regexReplace(State state) {
public String jacksonParseAndMask(State state) throws IOException {
return ParseAndMaskUtil.mask(
state.jsonString,
state.targetKeys,
JsonMaskingConfig.TargetKeyMode.MASK,
state.objectMapper
JsonMaskingConfig.getDefault(state.targetKeys)
).toString();
}
}
46 changes: 30 additions & 16 deletions src/main/java/dev/blaauwendraad/masker/json/KeyContainsMasker.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@
* {@link JsonMasker} implementation.
*/
public final class KeyContainsMasker implements JsonMasker {
/*
/**
* We are looking for targeted JSON keys with a maskable JSON value, so the closing quote can appear at minimum 3
* characters till the end of the JSON in the following minimal case: '{"":1}'
* characters till the end of the JSON in the following minimal case: '{"":1}'
*/
private static final int MIN_OFFSET_JSON_KEY_QUOTE = 3;
/*
/**
* Minimum JSON for which masking could be required is: {"":""}, so minimum length at least 7 bytes.
*/
private static final int MIN_MASKABLE_JSON_LENGTH = 7;

/**
* Look-up trie containing the target keys.
*/
private final ByteTrie targetKeysTrie;
private final boolean allowMode;
/**
* The masking configuration for the JSON masking process.
*/
private final JsonMaskingConfig maskingConfig;

/**
Expand All @@ -34,7 +38,6 @@ public final class KeyContainsMasker implements JsonMasker {
* @param maskingConfig the masking configurations for the created masker
*/
public KeyContainsMasker(JsonMaskingConfig maskingConfig) {
this.allowMode = maskingConfig.getTargetKeyMode() == JsonMaskingConfig.TargetKeyMode.ALLOW;
this.maskingConfig = maskingConfig;
this.targetKeysTrie = new ByteTrie(!maskingConfig.caseSensitiveTargetKeys());
for (String key : maskingConfig.getTargetKeys()) {
Expand Down Expand Up @@ -121,11 +124,11 @@ public byte[] mask(byte[] input) {
openingQuoteIndex + 1, // plus one for the opening quote
keyLength
);
if (allowMode && keyMatched) {
if (maskingConfig.isInAllowMode() && keyMatched) {
skipAllValues(maskingState); // the value belongs to a JSON key which is explicitly allowed, so skip it
continue;
}
if (!allowMode && !keyMatched) {
if (maskingConfig.isInMaskMode() && !keyMatched) {
continue mainLoop; // The found JSON key is not a target key, so continue looking from where we left of.
}

Expand Down Expand Up @@ -314,14 +317,14 @@ private void maskArrayValueInPlace(MaskingState maskingState) {
/**
* Masks all values (depending on the {@link JsonMaskingConfig} in the object.
*
* @param maskingState the current masking state
* @param maskingState the current masking state
*/
private void maskObjectValueInPlace(MaskingState maskingState) {
maskingState.incrementCurrentIndex(); // step over opening curly bracket
skipWhitespaceCharacters(maskingState);
while (!AsciiCharacter.isCurlyBracketClose(maskingState.byteAtCurrentIndex())) {
boolean valueMustBeMasked = true;
if (allowMode) {
if (maskingConfig.isInAllowMode()) {
// In case target keys should be considered as allow list, we need to NOT mask certain keys
int openingQuoteIndex = maskingState.currentIndex();
maskingState.incrementCurrentIndex(); // step over the JSON key opening quote
Expand Down Expand Up @@ -443,26 +446,37 @@ private static void skipAllValues(MaskingState maskingState) {
}
} else if (AsciiCharacter.isCurlyBracketOpen(maskingState.byteAtCurrentIndex())) { // object
maskingState.incrementCurrentIndex(); // step over opening curly bracket
// We need to specifically skip strings to not consider curly brackets which are part of a string
while (!AsciiCharacter.isCurlyBracketClose(maskingState.byteAtCurrentIndex())) {
int objectDepth = 1;
while (objectDepth > 0) {
// We need to specifically skip strings to not consider curly brackets which are part of a string
if (currentByteIsUnescapedDoubleQuote(maskingState)) {
// this makes sure that we skip curly brackets (open and close) which are part of strings
skipStringValue(maskingState);
} else {
if (AsciiCharacter.isCurlyBracketOpen(maskingState.byteAtCurrentIndex())) {
objectDepth++;
} else if (AsciiCharacter.isCurlyBracketClose(maskingState.byteAtCurrentIndex())) {
objectDepth--;
}
maskingState.incrementCurrentIndex();
}
}
maskingState.incrementCurrentIndex(); // step over closing curly bracket
} else if (AsciiCharacter.isSquareBracketOpen(maskingState.byteAtCurrentIndex())) { // array
maskingState.incrementCurrentIndex(); // step over opening square bracket
// We need to specifically skip strings to not consider square brackets which are part of a string
while (!AsciiCharacter.isSquareBracketClose(maskingState.byteAtCurrentIndex())) {
int arrayDepth = 1;
while (arrayDepth > 0) {
// We need to specifically skip strings to not consider square brackets which are part of a string
if (currentByteIsUnescapedDoubleQuote(maskingState)) {
skipStringValue(maskingState);
} else {
if (AsciiCharacter.isSquareBracketOpen(maskingState.byteAtCurrentIndex())) {
arrayDepth++;
} else if (AsciiCharacter.isSquareBracketClose(maskingState.byteAtCurrentIndex())) {
arrayDepth--;
}
maskingState.incrementCurrentIndex();
}
}
maskingState.incrementCurrentIndex(); // step over closing square bracket
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.blaauwendraad.masker.json.config;

import java.util.Set;
import java.util.stream.Collectors;

/**
* Contains the JSON masker configurations.
Expand Down Expand Up @@ -38,15 +37,11 @@ public class JsonMaskingConfig {
algorithmType = JsonMaskerAlgorithmType.KEYS_CONTAIN;
obfuscationLength = builder.obfuscationLength;
targetKeyMode = builder.targetKeyMode;
Set<String> targets = builder.targets;
if (targetKeyMode == TargetKeyMode.MASK && targets.isEmpty()) {
targetKeys = builder.targets;
if (targetKeyMode == TargetKeyMode.MASK && targetKeys.isEmpty()) {
throw new IllegalArgumentException("Target keys set in mask mode must contain at least a single target key");
}
caseSensitiveTargetKeys = builder.caseSensitiveTargetKeys;
if (!caseSensitiveTargetKeys) {
targets = targets.stream().map(String::toLowerCase).collect(Collectors.toSet());
}
targetKeys = targets;
maskNumericValuesWith = builder.maskNumberValuesWith;
if (builder.maskNumberValuesWith == 0) {
if (builder.obfuscationLength < 0 || builder.obfuscationLength > 1) {
Expand Down Expand Up @@ -228,6 +223,26 @@ public enum TargetKeyMode {
MASK
}

/**
* Checks if the target key mode is set to "ALLOW". If the mode is set to "ALLOW", it means that the target keys are
* interpreted as the only JSON keys for which the corresponding property is allowed (should not be masked).
*
* @return true if the target key mode is set to "ALLOW", false otherwise
*/
public boolean isInAllowMode() {
return targetKeyMode == TargetKeyMode.ALLOW;
}

/**
* Checks if the target key mode is set to "MASK". If the mode is set to "MASK", it means that the properties
* corresponding to the target keys should be masked.
*
* @return true if the current target key mode is in "MASK" mode, false otherwise
*/
public boolean isInMaskMode() {
return targetKeyMode == TargetKeyMode.MASK;
}

@Override
public String toString() {
return "JsonMaskingConfig{" +
Expand Down
92 changes: 28 additions & 64 deletions src/test/java/dev/blaauwendraad/masker/json/FuzzingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,51 @@
import com.fasterxml.jackson.databind.JsonNode;
import dev.blaauwendraad.masker.json.config.JsonMaskingConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.MethodSource;
import randomgen.json.RandomJsonGenerator;
import randomgen.json.RandomJsonGeneratorConfig;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
import java.util.stream.Stream;

//TODO
@Disabled("Have to implement object and array masking first in the Jackson masker")
final class FuzzingTest {
private static final int SECONDS_FOR_EACH_TEST_TO_RUN = 10;
private static final Set<String> DEFAULT_TARGET_KEYS = Set.of("targetKey1", "targetKey2", "targetKey3");
private static final int SECONDS_FOR_EACH_TEST_TO_RUN = 2;

@ValueSource(ints = { SECONDS_FOR_EACH_TEST_TO_RUN })
// duration in seconds the tests runs for
void fuzzing_NoArrayNoObjectValueMasking(int secondsToRunTest) {
long startTime = System.currentTimeMillis();
@ParameterizedTest
@MethodSource("jsonMaskingConfigs")
void fuzzingAgainstParseAndMaskUsingJackson(JsonMaskingConfig jsonMaskingConfig) {
Instant startTime = Instant.now();
int randomTestExecuted = 0;
while (System.currentTimeMillis() < startTime + 10 * 1000) {
Set<String> targetKeys = Set.of("targetKey1", "targetKey2");
JsonMasker keyContainsMasker = new KeyContainsMasker(JsonMaskingConfig.custom(
targetKeys,
JsonMaskingConfig.TargetKeyMode.MASK
).build());
RandomJsonGenerator randomJsonGenerator =
new RandomJsonGenerator(RandomJsonGeneratorConfig.builder().createConfig());
RandomJsonGenerator randomJsonGenerator = new RandomJsonGenerator(RandomJsonGeneratorConfig.builder()
.setTargetKeys(jsonMaskingConfig.getTargetKeys())
.createConfig());
JsonMasker masker = JsonMasker.getMasker(jsonMaskingConfig);
while (Duration.between(startTime, Instant.now()).getSeconds() < SECONDS_FOR_EACH_TEST_TO_RUN) {
JsonNode randomJsonNode = randomJsonGenerator.createRandomJsonNode();
String randomJsonNodeString = randomJsonNode.toPrettyString();
String keyContainsOutput = keyContainsMasker.mask(randomJsonNodeString);
String jacksonMaskingOutput = ParseAndMaskUtil.mask(
randomJsonNode,
targetKeys,
JsonMaskingConfig.TargetKeyMode.MASK
).toPrettyString();
Assertions.assertEquals(
jacksonMaskingOutput,
keyContainsOutput,
"Failed for input: " + randomJsonNodeString
String keyContainsOutput = masker.mask(randomJsonNodeString);
String jacksonMaskingOutput = ParseAndMaskUtil.mask(randomJsonNode, jsonMaskingConfig).toPrettyString();
Assertions.assertEquals(jacksonMaskingOutput,
keyContainsOutput,
"Failed for input: " + randomJsonNodeString
);
randomTestExecuted++;
}
System.out.printf(
"Executed %d randomly generated test scenarios in %d seconds%n",
randomTestExecuted,
secondsToRunTest
System.out.printf("Executed %d randomly generated test scenarios in %d seconds%n",
randomTestExecuted,
SECONDS_FOR_EACH_TEST_TO_RUN
);
}

@ParameterizedTest
@ValueSource(ints = { SECONDS_FOR_EACH_TEST_TO_RUN })
// duration in seconds the tests runs for
void fuzzing_AllowKeys_NoObjectArrayValuesMasking(int secondsToRunTest) {
long startTime = System.currentTimeMillis();
int randomTestExecuted = 0;
while (System.currentTimeMillis() < startTime + 10 * 1000) {
Set<String> targetKeys = Set.of("targetKey1", "targetKey2");
JsonMasker keyContainsMasker = new KeyContainsMasker(JsonMaskingConfig.custom(
targetKeys,
JsonMaskingConfig.TargetKeyMode.ALLOW
).build());
RandomJsonGenerator randomJsonGenerator =
new RandomJsonGenerator(RandomJsonGeneratorConfig.builder().createConfig());
JsonNode randomJsonNode = randomJsonGenerator.createRandomJsonNode();
String randomJsonNodeString = randomJsonNode.toPrettyString();
String keyContainsOutput = keyContainsMasker.mask(randomJsonNodeString);
String jacksonMaskingOutput = ParseAndMaskUtil.mask(
randomJsonNode,
targetKeys,
JsonMaskingConfig.TargetKeyMode.ALLOW
).toPrettyString();
Assertions.assertEquals(
jacksonMaskingOutput,
keyContainsOutput,
"Failed for input: " + randomJsonNodeString
);
randomTestExecuted++;
}
System.out.printf(
"Executed %d randomly generated test scenarios in %d seconds%n",
randomTestExecuted,
secondsToRunTest
@Nonnull
private static Stream<JsonMaskingConfig> jsonMaskingConfigs() {
return Stream.of(JsonMaskingConfig.getDefault(DEFAULT_TARGET_KEYS),
JsonMaskingConfig.custom(DEFAULT_TARGET_KEYS, JsonMaskingConfig.TargetKeyMode.ALLOW).build()
);
}
}
Loading