summaryrefslogtreecommitdiffstats
path: root/cps-rest
diff options
context:
space:
mode:
authorputhuparambil.aditya <aditya.puthuparambil@bell.ca>2021-03-16 12:01:23 +0000
committerToine Siebelink <toine.siebelink@est.tech>2021-04-06 09:16:35 +0000
commita3ceacb9ebf11c6467d66c0f42af714ef93591c2 (patch)
tree17fb41b04f5b56c61cca75cb6c746cd5abe17ed3 /cps-rest
parent2b0b0368cf6c62f8c05ed8a98a701e1102427861 (diff)
Fix for security hotspot related to safe archive expansion
https://sonarcloud.io/project/security_hotspots?id=onap_cps&hotspots=AXfObcsqA2pnU4Plp4-g Issue-ID: CPS-289 Signed-off-by: puthuparambil.aditya <aditya.puthuparambil@bell.ca> Change-Id: Ibe8627413fc9e3964cdc5bb98caf5e25fa4f3a95
Diffstat (limited to 'cps-rest')
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java24
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/utils/ZipFileSizeValidator.java81
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/utils/ZipFileSizeValidatorSpec.groovy101
3 files changed, 198 insertions, 8 deletions
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
index 532a0ca848..e3b0b2835c 100644
--- a/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Pantheon.tech
+ * Modifications Copyright (C) 2021 Bell Canada.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,21 +70,22 @@ public class MultipartFileUtil {
private static Map<String, String> extractYangResourcesMapFromZipArchive(final MultipartFile multipartFile) {
final ImmutableMap.Builder<String, String> yangResourceMapBuilder = ImmutableMap.builder();
-
+ final ZipFileSizeValidator zipFileSizeValidator = new ZipFileSizeValidator();
try (
final InputStream inputStream = multipartFile.getInputStream();
final ZipInputStream zipInputStream = new ZipInputStream(inputStream);
) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
- extractZipEntryToMapIfApplicable(yangResourceMapBuilder, zipEntry, zipInputStream);
+ extractZipEntryToMapIfApplicable(yangResourceMapBuilder, zipEntry, zipInputStream,
+ zipFileSizeValidator);
}
zipInputStream.closeEntry();
} catch (final IOException e) {
throw new CpsException("Cannot extract resources from zip archive.", e.getMessage(), e);
}
-
+ zipFileSizeValidator.validateSizeAndEntries();
try {
final Map<String, String> yangResourceMap = yangResourceMapBuilder.build();
if (yangResourceMap.isEmpty()) {
@@ -100,13 +102,13 @@ public class MultipartFileUtil {
private static void extractZipEntryToMapIfApplicable(
final ImmutableMap.Builder<String, String> yangResourceMapBuilder, final ZipEntry zipEntry,
- final ZipInputStream zipInputStream) throws IOException {
-
+ final ZipInputStream zipInputStream, final ZipFileSizeValidator zipFileSizeValidator) throws IOException {
+ zipFileSizeValidator.setCompressedSize(zipEntry.getCompressedSize());
final String yangResourceName = extractResourceNameFromPath(zipEntry.getName());
if (zipEntry.isDirectory() || !resourceNameEndsWithExtension(yangResourceName, YANG_FILE_EXTENSION)) {
return;
}
- yangResourceMapBuilder.put(yangResourceName, extractYangResourceContent(zipInputStream));
+ yangResourceMapBuilder.put(yangResourceName, extractYangResourceContent(zipInputStream, zipFileSizeValidator));
}
private static boolean resourceNameEndsWithExtension(final String resourceName, final String extension) {
@@ -125,12 +127,18 @@ public class MultipartFileUtil {
}
}
- private static String extractYangResourceContent(final ZipInputStream zipInputStream) throws IOException {
+ private static String extractYangResourceContent(final ZipInputStream zipInputStream,
+ final ZipFileSizeValidator zipFileSizeValidator) throws IOException {
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
- final byte[] buffer = new byte[READ_BUFFER_SIZE];
+ int totalSizeEntry = 0;
int numberOfBytesRead;
+ final byte[] buffer = new byte[READ_BUFFER_SIZE];
+ zipFileSizeValidator.incrementTotalEntryInArchive();
while ((numberOfBytesRead = zipInputStream.read(buffer, 0, READ_BUFFER_SIZE)) > 0) {
byteArrayOutputStream.write(buffer, 0, numberOfBytesRead);
+ totalSizeEntry += numberOfBytesRead;
+ zipFileSizeValidator.updateTotalSizeArchive(totalSizeEntry);
+ zipFileSizeValidator.validateCompresssionRatio(totalSizeEntry);
}
return byteArrayOutputStream.toString(StandardCharsets.UTF_8);
}
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/utils/ZipFileSizeValidator.java b/cps-rest/src/main/java/org/onap/cps/rest/utils/ZipFileSizeValidator.java
new file mode 100644
index 0000000000..d148fb70d4
--- /dev/null
+++ b/cps-rest/src/main/java/org/onap/cps/rest/utils/ZipFileSizeValidator.java
@@ -0,0 +1,81 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.rest.utils;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.onap.cps.spi.exceptions.ModelValidationException;
+
+@Setter
+@Getter
+public class ZipFileSizeValidator {
+
+ private static final int THRESHOLD_ENTRIES = 10000;
+ private static final int THRESHOLD_SIZE = 100000000;
+ private static final double THRESHOLD_RATIO = 40;
+ private static final String INVALID_ZIP = "Invalid ZIP archive content.";
+
+ private int totalSizeArchive = 0;
+ private int totalEntryInArchive = 0;
+ private long compressedSize = 0;
+
+ /**
+ * Increment the totalEntryInArchive by 1.
+ */
+ public void incrementTotalEntryInArchive() {
+ totalEntryInArchive++;
+ }
+
+ /**
+ * Update the totalSizeArchive by numberOfBytesRead.
+ * @param numberOfBytesRead the number of bytes of each entry
+ */
+ public void updateTotalSizeArchive(final int numberOfBytesRead) {
+ totalSizeArchive += numberOfBytesRead;
+ }
+
+ /**
+ * Validate the total Compression size of the zip.
+ * @param totalEntrySize the size of the unzipped entry.
+ */
+ public void validateCompresssionRatio(final int totalEntrySize) {
+ final double compressionRatio = (double) totalEntrySize / compressedSize;
+ if (compressionRatio > THRESHOLD_RATIO) {
+ throw new ModelValidationException(INVALID_ZIP,
+ String.format("Ratio between compressed and uncompressed data exceeds the CPS limit"
+ + " %s.", THRESHOLD_RATIO));
+ }
+ }
+
+ /**
+ * Validate the total Size and number of entries in the zip.
+ */
+ public void validateSizeAndEntries() {
+ if (totalSizeArchive > THRESHOLD_SIZE) {
+ throw new ModelValidationException(INVALID_ZIP,
+ String.format("The uncompressed data size exceeds the CPS limit %s bytes.", THRESHOLD_SIZE));
+ }
+ if (totalEntryInArchive > THRESHOLD_ENTRIES) {
+ throw new ModelValidationException(INVALID_ZIP,
+ String.format("The number of entries in the archive exceeds the CPS limit %s.",
+ THRESHOLD_ENTRIES));
+ }
+ }
+}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/utils/ZipFileSizeValidatorSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/ZipFileSizeValidatorSpec.groovy
new file mode 100644
index 0000000000..16fbf9885a
--- /dev/null
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/ZipFileSizeValidatorSpec.groovy
@@ -0,0 +1,101 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.rest.utils
+
+import org.onap.cps.spi.exceptions.ModelValidationException
+import spock.lang.Specification
+
+class ZipFileSizeValidatorSpec extends Specification {
+
+ def static thresholdSize = ZipFileSizeValidator.THRESHOLD_SIZE
+ def static thresholdEntries = ZipFileSizeValidator.THRESHOLD_ENTRIES
+ def static thresholdRatio = ZipFileSizeValidator.THRESHOLD_RATIO
+
+ def objectUnderTest = new ZipFileSizeValidator()
+ def compressedFileSize = 100
+
+ def setup() {
+ objectUnderTest.setTotalEntryInArchive(0)
+ objectUnderTest.setTotalSizeArchive(0)
+ objectUnderTest.setCompressedSize(compressedFileSize)
+ }
+
+ def 'Increment the total entries in Archive.'() {
+ when: 'the totalEntriesInArchive value is incremented'
+ objectUnderTest.incrementTotalEntryInArchive()
+ then: 'the totalEntriesInArchive is incremented by 1'
+ assert objectUnderTest.totalEntryInArchive == old(objectUnderTest.totalEntryInArchive) + 1
+ }
+
+ def 'Update the total size of Archive.'() {
+ given: 'the size of an entry of archive'
+ def entrySize = 100
+ when: 'the totalSizeArchive is to be updated with the latest entry Size'
+ objectUnderTest.updateTotalSizeArchive(entrySize)
+ then: 'the totalSizeArchive is updated as expected'
+ assert objectUnderTest.totalSizeArchive == old(objectUnderTest.totalSizeArchive) + entrySize
+ }
+
+ def 'Validate the zip archive for compression ratio less that threshold compression ratio.'() {
+ given: 'the totalEntrySize of the archive so that compression ratio is within the threshold'
+ int totalEntrySize = compressedFileSize * thresholdRatio - 1
+ when: 'the validation is performed against the threshold compression ratio'
+ objectUnderTest.validateCompresssionRatio(totalEntrySize)
+ then: 'validation passes and no exception is thrown'
+ noExceptionThrown()
+ }
+
+ def 'Validate the zip archive for compression ratio.'() {
+ given: 'the totalEntrySize of the archive so that compression ratio is higher than the threshold'
+ int totalEntrySize = compressedFileSize * thresholdRatio + 1
+ when: 'the validation is performed against the threshold compression ratio'
+ objectUnderTest.validateCompresssionRatio(totalEntrySize)
+ then: 'validation fails and exception is thrown'
+ thrown ModelValidationException
+ }
+
+ def 'Validate the zip archive for thresholdSize and thresholdEntries #caseDescriptor.'() {
+ given:
+ objectUnderTest.setTotalEntryInArchive(totalEntriesInArchive)
+ objectUnderTest.setTotalSizeArchive(totalSizeArchive)
+ when: 'the validation is performed against the threshold size and threshold Entries count'
+ objectUnderTest.validateSizeAndEntries()
+ then: 'validation passes and no exception is thrown'
+ noExceptionThrown()
+ where: 'following cases are tested'
+ caseDescriptor | totalSizeArchive | totalEntriesInArchive
+ 'less than threshold value' | thresholdSize - 1 | thresholdEntries - 1
+ 'at threshold value' | thresholdSize | thresholdEntries
+ }
+
+ def 'Validate the zip archive for thresholdSize and thresholdEntries with #caseDescriptor.'() {
+ given:
+ objectUnderTest.setTotalEntryInArchive(totalEntriesInArchive)
+ objectUnderTest.setTotalSizeArchive(totalSizeArchive)
+ when: 'the validation is performed against the threshold size and threshold Entries count'
+ objectUnderTest.validateSizeAndEntries()
+ then: 'validation fails and exception is thrown'
+ thrown ModelValidationException
+ where: 'following cases are tested'
+ caseDescriptor | totalSizeArchive | totalEntriesInArchive
+ 'totalEntriesInArchive exceeds threshold value' | thresholdSize | thresholdEntries + 1
+ 'totalSizeArchive exceeds threshold value' | thresholdSize + 1 | thresholdEntries
+ }
+}