Skip to content

Commit 6338ad1

Browse files
author
drighetto
committed
Add method to verify a json string #1
1 parent 52edafe commit 6338ad1

10 files changed

+794
-0
lines changed

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@
5454
<artifactId>poi</artifactId>
5555
<version>5.2.5</version>
5656
</dependency>
57+
<dependency>
58+
<groupId>javax.json</groupId>
59+
<artifactId>javax.json-api</artifactId>
60+
<version>1.1</version>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.glassfish</groupId>
64+
<artifactId>javax.json</artifactId>
65+
<version>1.1</version>
66+
</dependency>
67+
<!-- TEST ONLY PURPOSE -->
5768
<dependency>
5869
<groupId>org.junit.jupiter</groupId>
5970
<artifactId>junit-jupiter-engine</artifactId>

src/main/java/eu/righettod/SecurityUtils.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package eu.righettod;
22

3+
34
import org.apache.commons.csv.CSVFormat;
45
import org.apache.commons.csv.CSVRecord;
56
import org.apache.commons.validator.routines.InetAddressValidator;
@@ -30,6 +31,8 @@
3031

3132
import javax.crypto.Mac;
3233
import javax.crypto.spec.SecretKeySpec;
34+
import javax.json.Json;
35+
import javax.json.JsonReader;
3336
import javax.xml.parsers.DocumentBuilder;
3437
import javax.xml.parsers.DocumentBuilderFactory;
3538
import javax.xml.parsers.ParserConfigurationException;
@@ -737,4 +740,113 @@ public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingMode
737740
}
738741
return results;
739742
}
743+
744+
/**
745+
* Apply a collection of validations on a JSON string provided:<br>
746+
* - Real JSON structure.<br>
747+
* - Contain less than a specified number of deepness for nested objects or arrays.<br>
748+
* - Contain less than a specified number of items in any arrays.<br><br>
749+
*
750+
* <b>Note:</b> I decided to use a parsing approach using only string processing to prevent any StackOverFlow or OutOfMemory error that can be abused.<br><br>
751+
* I used the following assumption:
752+
* <ul>
753+
* <li>The character <code>{</code> identify the beginning of an object.</li>
754+
* <li>The character <code>}</code> identify the end of an object.</li>
755+
* <li>The character <code>[</code> identify the beginning of an array.</li>
756+
* <li>The character <code>]</code> identify the end of an array.</li>
757+
* <li>The character <code>"</code> identify the delimiter of a string.</li>
758+
* <li>The character sequence <code>\"</code> identify the escaping of an double quote.</li>
759+
* </ul>
760+
*
761+
* @param json String containing the JSON data to validate.
762+
* @param maxItemsByArraysCount Maximum number of items allowed in an array.
763+
* @param maxDeepnessAllowed Maximum number nested objects or arrays allowed.
764+
* @return True only if the string pass all validations.
765+
* @see "https://javaee.github.io/jsonp/"
766+
* @see "https://community.f5.com/discussions/technicalforum/disable-buffer-overflow-in-json-parameters/124306"
767+
* @see "https://github.com/InductiveComputerScience/pbJson/issues/2"
768+
*/
769+
public static boolean isJSONSafe(String json, int maxItemsByArraysCount, int maxDeepnessAllowed) {
770+
boolean isSafe = false;
771+
772+
try {
773+
//Step 1: Analyse the JSON string
774+
int currentDeepness = 0;
775+
int currentArrayItemsCount = 0;
776+
int maxDeepnessReached = 0;
777+
int maxArrayItemsCountReached = 0;
778+
boolean currentlyInArray = false;
779+
boolean currentlyInString = false;
780+
int currentNestedArrayLevel = 0;
781+
String jsonEscapedDoubleQuote = "\\\"";//Escaped double quote must not be considered as a string delimiter
782+
String work = json.replace(jsonEscapedDoubleQuote, "'");
783+
for (char c : work.toCharArray()) {
784+
switch (c) {
785+
case '{': {
786+
if (!currentlyInString) {
787+
currentDeepness++;
788+
}
789+
break;
790+
}
791+
case '}': {
792+
if (!currentlyInString) {
793+
currentDeepness--;
794+
}
795+
break;
796+
}
797+
case '[': {
798+
if (!currentlyInString) {
799+
currentDeepness++;
800+
if (currentlyInArray) {
801+
currentNestedArrayLevel++;
802+
}
803+
currentlyInArray = true;
804+
}
805+
break;
806+
}
807+
case ']': {
808+
if (!currentlyInString) {
809+
currentDeepness--;
810+
currentArrayItemsCount = 0;
811+
if (currentNestedArrayLevel > 0) {
812+
currentNestedArrayLevel--;
813+
}
814+
if (currentNestedArrayLevel == 0) {
815+
currentlyInArray = false;
816+
}
817+
}
818+
break;
819+
}
820+
case '"': {
821+
currentlyInString = !currentlyInString;
822+
break;
823+
}
824+
case ',': {
825+
if (!currentlyInString && currentlyInArray) {
826+
currentArrayItemsCount++;
827+
}
828+
break;
829+
}
830+
}
831+
if (currentDeepness > maxDeepnessReached) {
832+
maxDeepnessReached = currentDeepness;
833+
}
834+
if (currentArrayItemsCount > maxArrayItemsCountReached) {
835+
maxArrayItemsCountReached = currentArrayItemsCount;
836+
}
837+
}
838+
//Step 2: Apply validation against the value specified as limits
839+
isSafe = ((maxItemsByArraysCount > maxArrayItemsCountReached) && (maxDeepnessAllowed > maxDeepnessReached));
840+
841+
//Step 3: If the content is safe then ensure that it is valid JSON structure using the "Java API for JSON Processing" (JSR 374) parser reference implementation.
842+
if (isSafe) {
843+
JsonReader reader = Json.createReader(new StringReader(json));
844+
isSafe = (reader.read() != null);
845+
}
846+
847+
} catch (Exception e) {
848+
isSafe = false;
849+
}
850+
return isSafe;
851+
}
740852
}

src/test/java/eu/righettod/TestSecurityUtils.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,5 +346,43 @@ public void ensureSerializedObjectIntegrity() throws Exception {
346346
assertEquals(Boolean.FALSE, results.get("STATUS"));
347347
assertNotEquals(alteredInput, results.get("RESULT"));
348348
}
349+
350+
@Test
351+
public void isJSONSafe() {
352+
final int maxItems = 10;
353+
//Test unsafe json files
354+
List<String> testUnsafeJsonFiles = List.of("test-json-100arrayitems.json", "test-json-100nestedobjects.json", "test-json-100nestedarrays.json", "test-json-50000nestedobjects.json");
355+
testUnsafeJsonFiles.forEach(f -> {
356+
try {
357+
String testFile = getTestFilePath(f);
358+
boolean isSafe = SecurityUtils.isJSONSafe(Files.readString(Paths.get(testFile)), maxItems, maxItems);
359+
assertFalse(isSafe, String.format(TEMPLATE_MESSAGE_FALSE_NEGATIVE_FOR_FILE, f));
360+
} catch (IOException e) {
361+
throw new RuntimeException(e);
362+
}
363+
});
364+
//Test safe but invalid json files
365+
List<String> testSafeButInvalidJsonFiles = List.of("test-json-safebutinvalid.json");
366+
testSafeButInvalidJsonFiles.forEach(f -> {
367+
try {
368+
String testFile = getTestFilePath(f);
369+
boolean isSafe = SecurityUtils.isJSONSafe(Files.readString(Paths.get(testFile)), maxItems, maxItems);
370+
assertFalse(isSafe, String.format(TEMPLATE_MESSAGE_FALSE_NEGATIVE_FOR_FILE, f));
371+
} catch (IOException e) {
372+
throw new RuntimeException(e);
373+
}
374+
});
375+
//Test safe json files
376+
List<String> testSafeJsonFiles = List.of("test-json-escapeddoublequotes.json", "test-json-safe00.json");
377+
testSafeJsonFiles.forEach(f -> {
378+
try {
379+
String testFile = getTestFilePath(f);
380+
boolean isSafe = SecurityUtils.isJSONSafe(Files.readString(Paths.get(testFile)), maxItems, maxItems);
381+
assertTrue(isSafe, String.format(TEMPLATE_MESSAGE_FALSE_POSITIVE_FOR_FILE, f));
382+
} catch (IOException e) {
383+
throw new RuntimeException(e);
384+
}
385+
});
386+
}
349387
}
350388

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
[
2+
"ITEM001",
3+
"ITEM002",
4+
"ITEM003",
5+
"ITEM004",
6+
"ITEM005",
7+
"ITEM006",
8+
"ITEM007",
9+
"ITEM008",
10+
"ITEM009",
11+
"ITEM010",
12+
"ITEM011",
13+
"ITEM012",
14+
"ITEM013",
15+
"ITEM014",
16+
"ITEM015",
17+
"ITEM016",
18+
"ITEM017",
19+
"ITEM018",
20+
"ITEM019",
21+
"ITEM020",
22+
"ITEM021",
23+
"ITEM022",
24+
"ITEM023",
25+
"ITEM024",
26+
"ITEM025",
27+
"ITEM026",
28+
"ITEM027",
29+
"ITEM028",
30+
"ITEM029",
31+
"ITEM030",
32+
"ITEM031",
33+
"ITEM032",
34+
"ITEM033",
35+
"ITEM034",
36+
"ITEM035",
37+
"ITEM036",
38+
"ITEM037",
39+
"ITEM038",
40+
"ITEM039",
41+
"ITEM040",
42+
"ITEM041",
43+
"ITEM042",
44+
"ITEM043",
45+
"ITEM044",
46+
"ITEM045",
47+
"ITEM046",
48+
"ITEM047",
49+
"ITEM048",
50+
"ITEM049",
51+
"ITEM050",
52+
"ITEM051",
53+
"ITEM052",
54+
"ITEM053",
55+
"ITEM054",
56+
"ITEM055",
57+
"ITEM056",
58+
"ITEM057",
59+
"ITEM058",
60+
"ITEM059",
61+
"ITEM060",
62+
"ITEM061",
63+
"ITEM062",
64+
"ITEM063",
65+
"ITEM064",
66+
"ITEM065",
67+
"ITEM066",
68+
"ITEM067",
69+
"ITEM068",
70+
"ITEM069",
71+
"ITEM070",
72+
"ITEM071",
73+
"ITEM072",
74+
"ITEM073",
75+
"ITEM074",
76+
"ITEM075",
77+
"ITEM076",
78+
"ITEM077",
79+
"ITEM078",
80+
"ITEM079",
81+
"ITEM080",
82+
"ITEM081",
83+
"ITEM082",
84+
"ITEM083",
85+
"ITEM084",
86+
"ITEM085",
87+
"ITEM086",
88+
"ITEM087",
89+
"ITEM088",
90+
"ITEM089",
91+
"ITEM090",
92+
"ITEM091",
93+
"ITEM092",
94+
"ITEM093",
95+
"ITEM094",
96+
"ITEM095",
97+
"ITEM096",
98+
"ITEM097",
99+
"ITEM098",
100+
"ITEM099",
101+
"XXX"
102+
]

0 commit comments

Comments
 (0)