From a3ceacb9ebf11c6467d66c0f42af714ef93591c2 Mon Sep 17 00:00:00 2001 From: "puthuparambil.aditya" Date: Tue, 16 Mar 2021 12:01:23 +0000 Subject: 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 Change-Id: Ibe8627413fc9e3964cdc5bb98caf5e25fa4f3a95 --- .../org/onap/cps/rest/utils/MultipartFileUtil.java | 24 ++++--- .../onap/cps/rest/utils/ZipFileSizeValidator.java | 81 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 cps-rest/src/main/java/org/onap/cps/rest/utils/ZipFileSizeValidator.java (limited to 'cps-rest/src/main') 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 532a0ca84..e3b0b2835 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 extractYangResourcesMapFromZipArchive(final MultipartFile multipartFile) { final ImmutableMap.Builder 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 yangResourceMap = yangResourceMapBuilder.build(); if (yangResourceMap.isEmpty()) { @@ -100,13 +102,13 @@ public class MultipartFileUtil { private static void extractZipEntryToMapIfApplicable( final ImmutableMap.Builder 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 000000000..d148fb70d --- /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)); + } + } +} -- cgit 1.2.3-korg