Skip to content

Commit e1b03d3

Browse files
committed
Add new method
1 parent 3048354 commit e1b03d3

7 files changed

Lines changed: 2213 additions & 1603 deletions

File tree

docs/eu/righettod/ProcessingMode.html

Lines changed: 312 additions & 229 deletions
Large diffs are not rendered by default.

docs/eu/righettod/SecurityUtils.html

Lines changed: 1648 additions & 1232 deletions
Large diffs are not rendered by default.

docs/eu/righettod/class-use/ProcessingMode.html

Lines changed: 127 additions & 102 deletions
Large diffs are not rendered by default.

docs/src-html/eu/righettod/SecurityUtils.html

Lines changed: 36 additions & 25 deletions
Large diffs are not rendered by default.

src/main/java/eu/righettod/ProcessingMode.java renamed to src/main/java/eu/righettod/ProcessingModeType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* Enumeration used by the method <code>SecurityUtils.ensureSerializedObjectIntegrity()</code> to define its working mode.
55
*/
6-
public enum ProcessingMode {
6+
public enum ProcessingModeType {
77
/**
88
* Protection mode: Add the integrity HMAC to the linked serialized object.
99
*/

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import java.util.concurrent.atomic.AtomicInteger;
7575
import java.util.regex.Matcher;
7676
import java.util.regex.Pattern;
77+
import java.util.zip.GZIPInputStream;
7778
import java.util.zip.ZipEntry;
7879
import java.util.zip.ZipFile;
7980

@@ -735,9 +736,9 @@ public static boolean isExcelCSVSafe(String csvFilePath) {
735736
* Provide a way to add an integrity marker (<a href="https://en.wikipedia.org/wiki/HMAC">HMAC</a>) to a serialized object serialized using the <a href="https://www.baeldung.com/java-serialization">java native system</a> (binary).<br>
736737
* The goal is to provide <b>a temporary workaround</b> to try to prevent deserialization attacks and give time to move to a text-based serialization approach.
737738
*
738-
* @param processingMode Define the mode of processing i.e. protect or validate. ({@link eu.righettod.ProcessingMode})
739-
* @param input When the processing mode is "protect" than the expected input (string) is a java serialized object encoded in Base64 otherwise (processing mode is "validate") expected input is the output of this method when the "protect" mode was used.
740-
* @param secret Secret to use to compute the SHA256 HMAC.
739+
* @param processingModeType Define the mode of processing i.e. protect or validate. ({@link ProcessingModeType})
740+
* @param input When the processing mode is "protect" than the expected input (string) is a java serialized object encoded in Base64 otherwise (processing mode is "validate") expected input is the output of this method when the "protect" mode was used.
741+
* @param secret Secret to use to compute the SHA256 HMAC.
741742
* @return A map with the following keys: <ul><li><b>PROCESSING_MODE</b>: Processing mode used to compute the result.</li><li><b>STATUS</b>: A boolean indicating if the processing was successful or not.</li><li><b>RESULT</b>: Always contains a string representing the protected serialized object in the format <code>[SERIALIZED_OBJECT_BASE64_ENCODED]:[SERIALIZED_OBJECT_HMAC_BASE64_ENCODED]</code>.</li></ul>
742743
* @throws Exception If any exception occurs.
743744
* @see "https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html"
@@ -749,11 +750,11 @@ public static boolean isExcelCSVSafe(String csvFilePath) {
749750
* @see "https://en.wikipedia.org/wiki/HMAC"
750751
* @see "https://smattme.com/posts/how-to-generate-hmac-signature-in-java/"
751752
*/
752-
public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingMode processingMode, String input, byte[] secret) throws Exception {
753+
public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingModeType processingModeType, String input, byte[] secret) throws Exception {
753754
Map<String, Object> results;
754755
String resultFormatTemplate = "%s:%s";
755756
//Verify input provided to be consistent
756-
if (processingMode == null) {
757+
if (processingModeType == null) {
757758
throw new IllegalArgumentException("The processing mode is mandatory!");
758759
}
759760
if (input == null || input.trim().isEmpty()) {
@@ -762,7 +763,7 @@ public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingMode
762763
if (secret == null || secret.length == 0) {
763764
throw new IllegalArgumentException("The HMAC secret is mandatory!");
764765
}
765-
if (processingMode.equals(ProcessingMode.VALIDATE) && input.split(":").length != 2) {
766+
if (processingModeType.equals(ProcessingModeType.VALIDATE) && input.split(":").length != 2) {
766767
throw new IllegalArgumentException("Input data provided is invalid for the processing mode specified!");
767768
}
768769
//Processing
@@ -773,8 +774,8 @@ public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingMode
773774
SecretKeySpec key = new SecretKeySpec(secret, hmacAlgorithm);
774775
mac.init(key);
775776
results = new HashMap<>();
776-
results.put("PROCESSING_MODE", processingMode.toString());
777-
switch (processingMode) {
777+
results.put("PROCESSING_MODE", processingModeType.toString());
778+
switch (processingModeType) {
778779
case PROTECT -> {
779780
byte[] objectBytes = b64Decoder.decode(input);
780781
byte[] hmac = mac.doFinal(objectBytes);
@@ -1542,4 +1543,43 @@ public static Map<SensitiveInformationType, Set<String>> extractAllSensitiveInfo
15421543

15431544
return data;
15441545
}
1546+
1547+
/**
1548+
* Apply a collection of validations on a bytes array provided representing GZIP compressed data:
1549+
* <ul>
1550+
* <li>Are valid GZIP compressed data.</li>
1551+
* <li>The number of bytes once decompressed is under the specified limit.</li>
1552+
* </ul>
1553+
* <br><b>Note:</b> The value <code>Integer.MAX_VALUE - 8</code> was chosen because during my tests on Java 25 (JDK 64 bits on Windows 11 Pro), it was possible to decompress such amount of data with the default JVM settings without causing an <a href="https://docs.oracle.com/en/java/javase/25/docs/api//java.base/java/lang/OutOfMemoryError.html">Out Of Memory error</a>.
1554+
*
1555+
* @param compressedBytes Array of bytes containing the GZIP compressed data to check.
1556+
* @param maxCountOfDecompressedBytesAllowed Maximum number of decompressed bytes allowed. Default to 10 MB if the specified value is inferior to 1 or superior to Integer.MAX_VALUE - 8.
1557+
* @return True only if the file pass all validations.
1558+
* @see "https://en.wikipedia.org/wiki/Gzip"
1559+
* @see "https://www.rapid7.com/db/modules/auxiliary/dos/http/gzip_bomb_dos/"
1560+
*/
1561+
public static boolean isGZIPCompressedDataSafe(byte[] compressedBytes, long maxCountOfDecompressedBytesAllowed) {
1562+
boolean isSafe = false;
1563+
try {
1564+
long limit = maxCountOfDecompressedBytesAllowed;
1565+
long totalRead = 0L;
1566+
byte[] buffer = new byte[8 * 1024];
1567+
int read;
1568+
if (limit < 1 || limit > (Integer.MAX_VALUE - 8)) {
1569+
limit = 10_000_000;
1570+
}
1571+
try (ByteArrayInputStream bis = new ByteArrayInputStream(compressedBytes); GZIPInputStream gzipInputStream = new GZIPInputStream(new BufferedInputStream(bis))) {
1572+
while ((read = gzipInputStream.read(buffer)) != -1) {
1573+
totalRead += read;
1574+
if (totalRead > limit) {
1575+
throw new Exception();
1576+
}
1577+
}
1578+
}
1579+
isSafe = true;
1580+
} catch (Exception e) {
1581+
isSafe = false;
1582+
}
1583+
return isSafe;
1584+
}
15451585
}

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.time.ZoneId;
2929
import java.time.format.DateTimeFormatter;
3030
import java.util.*;
31+
import java.util.zip.GZIPOutputStream;
3132

3233
import static org.junit.jupiter.api.Assertions.*;
3334

@@ -55,6 +56,15 @@ private long getTestFileSize(String testFileName) {
5556
return new File(getTestFilePath(testFileName)).length();
5657
}
5758

59+
private byte[] createGZipCompressedData(int uncompressedDataSizeWanted) throws Exception {
60+
byte[] raw = new byte[uncompressedDataSizeWanted];
61+
Arrays.fill(raw, (byte) 'X');
62+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
63+
gzos.write(raw);
64+
gzos.finish();
65+
return baos.toByteArray();
66+
}
67+
}
5868

5969
@Test
6070
public void isWeakPINCode() {
@@ -339,9 +349,9 @@ public void ensureSerializedObjectIntegrity() throws Exception {
339349
Base64.Decoder b64Decoder = Base64.getDecoder();
340350
String testUserSerializedBytesEncoded = b64Encoder.encodeToString(testUserSerializedBytes.toByteArray());
341351
//Test "protect" processing
342-
Map<String, Object> results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingMode.PROTECT, testUserSerializedBytesEncoded, secret);
352+
Map<String, Object> results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingModeType.PROTECT, testUserSerializedBytesEncoded, secret);
343353
assertEquals(3, results.size());
344-
assertEquals(ProcessingMode.PROTECT.toString(), results.get("PROCESSING_MODE"));
354+
assertEquals(ProcessingModeType.PROTECT.toString(), results.get("PROCESSING_MODE"));
345355
assertEquals(Boolean.TRUE, results.get("STATUS"));
346356
String protectedSerializedObject = (String) results.get("RESULT");
347357
String[] parts = protectedSerializedObject.split(":");
@@ -350,9 +360,9 @@ public void ensureSerializedObjectIntegrity() throws Exception {
350360
assertNotEquals(0, b64Decoder.decode(parts[1]).length);
351361
//Test "validate" processing
352362
//--Case validation succeed (HMAC match)
353-
results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingMode.VALIDATE, protectedSerializedObject, secret);
363+
results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingModeType.VALIDATE, protectedSerializedObject, secret);
354364
assertEquals(3, results.size());
355-
assertEquals(ProcessingMode.VALIDATE.toString(), results.get("PROCESSING_MODE"));
365+
assertEquals(ProcessingModeType.VALIDATE.toString(), results.get("PROCESSING_MODE"));
356366
assertEquals(Boolean.TRUE, results.get("STATUS"));
357367
assertEquals(protectedSerializedObject, results.get("RESULT"));
358368
//--Case validation failed due to malicious serialized object provided (HMAC not match)
@@ -363,8 +373,8 @@ public void ensureSerializedObjectIntegrity() throws Exception {
363373
alteredObject[0] += 1;
364374
encodedSerializedObject = b64Encoder.encodeToString(alteredObject);
365375
String alteredInput = encodedSerializedObject + ":" + encodedHMAC;
366-
results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingMode.VALIDATE, alteredInput, secret);
367-
assertEquals(ProcessingMode.VALIDATE.toString(), results.get("PROCESSING_MODE"));
376+
results = SecurityUtils.ensureSerializedObjectIntegrity(ProcessingModeType.VALIDATE, alteredInput, secret);
377+
assertEquals(ProcessingModeType.VALIDATE.toString(), results.get("PROCESSING_MODE"));
368378
assertEquals(Boolean.FALSE, results.get("STATUS"));
369379
assertNotEquals(alteredInput, results.get("RESULT"));
370380
}
@@ -748,7 +758,32 @@ public void extractAllSensitiveInformation() {
748758
throw new RuntimeException(e);
749759
}
750760
});
761+
}
751762

763+
@Test
764+
public void isGZIPCompressedDataSafe() throws Exception {
765+
String falseNegativeMsgTemplate = "Array of %s bytes with limit to %s bytes must be detected as unsafe!";
766+
String falsePositiveMsgTemplate = "Array of %s bytes with limit to %s bytes must be detected as safe!";
767+
//Test invalid cases
768+
//--Large compressed data > limit of 5MB
769+
byte[] testData = createGZipCompressedData(Integer.MAX_VALUE - 2);
770+
long limit = 5_000_000;//5 MB
771+
boolean isSafe = SecurityUtils.isGZIPCompressedDataSafe(testData, limit);
772+
assertFalse(isSafe, String.format(falseNegativeMsgTemplate, testData.length, limit));
773+
//--Large compressed data > default limit of 10 MB
774+
limit = 0;//trigger the usage of the default limit
775+
isSafe = SecurityUtils.isGZIPCompressedDataSafe(testData, limit);
776+
assertFalse(isSafe, String.format(falseNegativeMsgTemplate, testData.length, limit));
777+
//Test valid cases
778+
//--2MB initial data compressed < limit of 5MB
779+
testData = createGZipCompressedData(2_000_000);//2 MB
780+
limit = 5_000_000;//5 MB
781+
isSafe = SecurityUtils.isGZIPCompressedDataSafe(testData, limit);
782+
assertTrue(isSafe, String.format(falsePositiveMsgTemplate, testData.length, limit));
783+
//--2MB initial data compressed < default limit of 10 MB
784+
limit = 0;//trigger the usage of the default limit
785+
isSafe = SecurityUtils.isGZIPCompressedDataSafe(testData, limit);
786+
assertTrue(isSafe, String.format(falsePositiveMsgTemplate, testData.length, limit));
752787
}
753788
}
754789

0 commit comments

Comments
 (0)