diff options
Diffstat (limited to 'common-app-api')
-rw-r--r-- | common-app-api/pom.xml | 14 | ||||
-rw-r--r-- | common-app-api/src/main/java/org/openecomp/sdc/common/util/ZipUtil.java | 135 | ||||
-rw-r--r-- | common-app-api/src/main/java/org/openecomp/sdc/common/zip/ZipUtils.java | 354 | ||||
-rw-r--r-- | common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipException.java | 31 | ||||
-rw-r--r-- | common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipSlipException.java | 27 | ||||
-rw-r--r-- | common-app-api/src/test/java/org/openecomp/sdc/common/util/ZipUtilTest.java | 67 | ||||
-rw-r--r-- | common-app-api/src/test/java/org/openecomp/sdc/common/zip/ZipUtilsTest.java | 172 | ||||
-rw-r--r-- | common-app-api/src/test/resources/zip-slip/zip-slip-linux.zip | bin | 0 -> 545 bytes | |||
-rw-r--r-- | common-app-api/src/test/resources/zip-slip/zip-slip-windows.zip | bin | 0 -> 547 bytes | |||
-rw-r--r-- | common-app-api/src/test/resources/zip/extract-test.zip | bin | 0 -> 2588 bytes |
10 files changed, 598 insertions, 202 deletions
diff --git a/common-app-api/pom.xml b/common-app-api/pom.xml index 38d3bbec87..62bfb24b28 100644 --- a/common-app-api/pom.xml +++ b/common-app-api/pom.xml @@ -159,6 +159,20 @@ </dependency> <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest</artifactId> + <version>${hamcrest.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <version>${hamcrest.version}</version> + <scope>test</scope> + </dependency> + + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> diff --git a/common-app-api/src/main/java/org/openecomp/sdc/common/util/ZipUtil.java b/common-app-api/src/main/java/org/openecomp/sdc/common/util/ZipUtil.java deleted file mode 100644 index ec3c31cffe..0000000000 --- a/common-app-api/src/main/java/org/openecomp/sdc/common/util/ZipUtil.java +++ /dev/null @@ -1,135 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.openecomp.sdc.common.util; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.ByteArrayOutputStream; -import org.openecomp.sdc.common.log.wrappers.Logger; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -public class ZipUtil { - - private static final int KB = 1024; - private static Logger log = Logger.getLogger(ZipUtil.class.getName()); - - private ZipUtil() { - } - - public static Map<String, byte[]> readZip(File file) { - try (InputStream fileInputStream = new FileInputStream(file)) { - return readZip(IOUtils.toByteArray(fileInputStream)); - } catch (IOException e) { - log.info("close File stream failed - {}", e); - return null; - } - } - - public static Map<String, byte[]> readZip(byte[] zipAsBytes) { - Map<String, byte[]> fileNameToByteArray = new HashMap<>(); - byte[] buffer = new byte[KB]; - try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipAsBytes); - ZipInputStream zis = new ZipInputStream(byteArrayInputStream)) { - // get the zipped file list entry - ZipEntry ze = zis.getNextEntry(); - - while (ze != null) { - - String fileName = ze.getName(); - - if (!ze.isDirectory()) { - - try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - int len; - while ((len = zis.read(buffer)) > 0) { - os.write(buffer, 0, len); - } - - fileNameToByteArray.put(fileName, os.toByteArray()); - - } - } - ze = zis.getNextEntry(); - } - } catch (IOException ex) { - log.info("close Byte stream failed", ex); - return null; - } - - return fileNameToByteArray; - - } - - public static void main(String[] args) { - String zipFileName = "/src/test/resources/config/config.zip"; - zipFileName = "C:\\Git_work\\D2-SDnC\\catalog-be\\src\\test\\resources\\config\\config.zip"; - Path path = Paths.get(zipFileName); - - try { - byte[] zipAsBytes = Files.readAllBytes(path); - // encode to base - - ZipUtil.readZip(zipAsBytes); - - } catch (IOException e) { - log.info("close Byte stream failed", e); - } - } - - public static byte[] zipBytes(byte[] input) throws IOException { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zos = new ZipOutputStream(baos)) { - ZipEntry entry = new ZipEntry("zip"); - entry.setSize(input.length); - zos.putNextEntry(entry); - zos.write(input); - zos.closeEntry(); - return baos.toByteArray(); - } - } - - public static byte[] unzip(byte[] zipped) { - try (ZipInputStream zipinputstream = new ZipInputStream(new ByteArrayInputStream(zipped)); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - byte[] buf = new byte[KB]; - ZipEntry zipentry = zipinputstream.getNextEntry(); - int n; - while ((n = zipinputstream.read(buf, 0, KB)) > -1) { - outputStream.write(buf, 0, n); - } - return outputStream.toByteArray(); - } catch (Exception e) { - throw new IllegalStateException("Can't unzip input stream", e); - } - } -} diff --git a/common-app-api/src/main/java/org/openecomp/sdc/common/zip/ZipUtils.java b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/ZipUtils.java new file mode 100644 index 0000000000..d90377fc88 --- /dev/null +++ b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/ZipUtils.java @@ -0,0 +1,354 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.common.zip; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.IOUtils; +import org.openecomp.sdc.common.zip.exception.ZipException; +import org.openecomp.sdc.common.zip.exception.ZipSlipException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles zip operations. + */ +public class ZipUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(ZipUtils.class); + + private ZipUtils() { + } + + /** + * Checks if the path is a zip slip attempt calling the {@link #checkForZipSlipInRead(Path)} method. + * @param zipEntry the zip entry + * @throws ZipSlipException when a zip slip attempt is detected + */ + public static void checkForZipSlipInRead(final ZipEntry zipEntry) throws ZipSlipException { + final Path filePath = Paths.get(zipEntry.getName()); + checkForZipSlipInRead(filePath); + } + + /** + * Checks if the path is a zip slip attempt when you don't have a destination folder eg in memory reading or zip + * creation. + * + * @param filePath the file path + * @throws ZipSlipException when a zip slip attempt is detected + */ + public static void checkForZipSlipInRead(final Path filePath) throws ZipSlipException { + final File file = filePath.toFile(); + String canonicalPath = null; + try { + canonicalPath = file.getCanonicalPath(); + } catch (final IOException ignored) { + //ignored + } + if (canonicalPath != null && !canonicalPath.equals(file.getAbsolutePath())) { + throw new ZipSlipException(filePath.toString()); + } + + if (filePath.toString().contains("../") || filePath.toString().contains("..\\")) { + throw new ZipSlipException(filePath.toString()); + } + } + + /** + * Checks if the zip entry is a zip slip attempt based on the destination directory. + * + * @param zipEntry the zip entry + * @param targetDirectoryPath the target extraction folder + * @throws ZipException when the zip slip was detected as a {@link ZipSlipException}. Also when there was a problem + * getting the canonical paths from the zip entry or target directory. + */ + public static void checkForZipSlipInExtraction(final ZipEntry zipEntry, + final Path targetDirectoryPath) throws ZipException { + final File targetDirectoryAsFile = targetDirectoryPath.toFile(); + final File targetFile = new File(targetDirectoryAsFile, zipEntry.getName()); + final String targetDirectoryCanonicalPath; + try { + targetDirectoryCanonicalPath = targetDirectoryAsFile.getCanonicalPath(); + } catch (final IOException e) { + throw new ZipException( + String.format("Could not obtain canonical path of: '%s'", targetDirectoryAsFile.getAbsolutePath()), e); + } + final String targetFileCanonicalPath; + try { + targetFileCanonicalPath = targetFile.getCanonicalPath(); + } catch (final IOException e) { + throw new ZipException( + String.format("Could not obtain canonical path of: '%s'", targetFile.getAbsolutePath()), e); + } + + if (!targetFileCanonicalPath.startsWith(targetDirectoryCanonicalPath + File.separator)) { + throw new ZipSlipException(zipEntry.getName()); + } + } + + /** + * Creates a ZipInputStream from a byte array. + * + * @param zipFileBytes the zip byte array + * @return the created ZipInputStream. + */ + private static ZipInputStream getInputStreamFromBytes(final byte[] zipFileBytes) { + return new ZipInputStream(new ByteArrayInputStream(zipFileBytes)); + } + + /** + * Reads a zip file into memory. Parses the zipFile in byte array and calls {@link #readZip(byte[], boolean)}. + * + * @param zipFile the zip file to read + * @param hasToIncludeDirectories includes or not the directories found during the zip reading + * @return a Map representing a pair of file path and file byte array + * @throws ZipException when there was a problem during the reading process + */ + public static Map<String, byte[]> readZip(final File zipFile, + final boolean hasToIncludeDirectories) throws ZipException { + try { + return readZip(Files.readAllBytes(zipFile.toPath()), hasToIncludeDirectories); + } catch (final IOException e) { + throw new ZipException(String.format("Could not read the zip file '%s'", zipFile.getName()), e); + } + } + + /** + * Reads a zip file to a in memory structure formed by the file path and its bytes. The structure can contains only + * files or files and directories. If configured to include directories, only empty directories and directories that + * contains files will be included. The full directory tree will not be generated, eg: + * <pre> + * \ + * \..\Directory + * \..\..\ChildDirectory + * \..\..\..\aFile.txt + * \..\..\EmptyChildDirectory + * </pre> + * The return will include "Directory\ChildDirectory\aFile.txt" and "Directory\EmptyChildDirectory" but not + * "Directory" or the root. + * + * @param zipFileBytes the zip file byte array to read + * @param hasToIncludeDirectories includes or not the directories found during the zip reading. + * @return a Map representing a pair of file path and file byte array + * @throws ZipException when there was a problem during the reading process + */ + public static Map<String, byte[]> readZip(final byte[] zipFileBytes, + final boolean hasToIncludeDirectories) throws ZipException { + final Map<String, byte[]> filePathAndByteMap = new HashMap<>(); + + try (final ZipInputStream inputZipStream = ZipUtils.getInputStreamFromBytes(zipFileBytes)) { + byte[] fileByteContent; + String currentEntryName; + ZipEntry zipEntry; + while ((zipEntry = inputZipStream.getNextEntry()) != null) { + checkForZipSlipInRead(zipEntry); + currentEntryName = zipEntry.getName(); + fileByteContent = getBytes(inputZipStream); + if (zipEntry.isDirectory()) { + if (hasToIncludeDirectories) { + filePathAndByteMap.put(normalizeFolder(currentEntryName), null); + } + } else { + if (hasToIncludeDirectories) { + final Path parentFolderPath = Paths.get(zipEntry.getName()).getParent(); + if (parentFolderPath != null) { + filePathAndByteMap.putIfAbsent(normalizeFolder(parentFolderPath.toString()), null); + } + } + filePathAndByteMap.put(currentEntryName, fileByteContent); + } + } + } catch (final IOException e) { + LOGGER.warn("Could not close the zip input stream", e); + } + + return filePathAndByteMap; + } + + /** + * Adds a {@link File#separator} at the end of the folder path if not present. + * + * @param folderPath the folder to normalize + * @return the normalized folder + */ + private static String normalizeFolder(final String folderPath) { + final StringBuilder normalizedFolderBuilder = new StringBuilder(folderPath); + if(!folderPath.endsWith(File.separator)) { + normalizedFolderBuilder.append(File.separator); + } + return normalizedFolderBuilder.toString(); + } + + /** + * Converts a ZipInputStream in byte array. + * + * @param inputZipStream the zip input stream + * @return the byte array representing the input stream + * @throws ZipException when there was a problem parsing the input zip stream + */ + private static byte[] getBytes(final ZipInputStream inputZipStream) throws ZipException { + final byte[] fileByteContent; + try { + fileByteContent = IOUtils.toByteArray(inputZipStream); + } catch (final IOException e) { + throw new ZipException("Could not read bytes from file", e); + } + return fileByteContent; + } + + /** + * Unzips a zip file into an output folder. + * + * @param zipFilePath the zip file path + * @param outputFolder the output folder path + * @throws ZipException when there was a problem during the unzip process + */ + public static void unzip(final Path zipFilePath, final Path outputFolder) throws ZipException { + if (zipFilePath == null || outputFolder == null) { + return; + } + createDirectoryIfNotExists(outputFolder); + + final File zipFile = zipFilePath.toFile(); + try (final FileInputStream fileInputStream = new FileInputStream(zipFile); + final ZipInputStream stream = new ZipInputStream(fileInputStream)) { + + ZipEntry zipEntry; + while ((zipEntry = stream.getNextEntry()) != null) { + checkForZipSlipInExtraction(zipEntry, outputFolder); + final String fileName = zipEntry.getName(); + final Path fileToWritePath = Paths.get(outputFolder.toString(), fileName); + if (zipEntry.isDirectory()) { + createDirectoryIfNotExists(fileToWritePath); + } else { + writeFile(stream, fileToWritePath); + } + } + } catch (final FileNotFoundException e) { + throw new ZipException(String.format("Could not find file: '%s'", zipFile.getAbsolutePath()), e); + } catch (final IOException e) { + throw new ZipException( + String.format("An unexpected error occurred trying to unzip '%s'", zipFile.getAbsolutePath()), e); + } + } + + /** + * Writes a file from a zipInputStream to a path. Creates the file parent directories if they don't exist. + * @param zipInputStream the zip input stream + * @param fileToWritePath the file path to write + * @throws ZipException when there was a problem during the file creation + */ + private static void writeFile(final ZipInputStream zipInputStream, final Path fileToWritePath) throws ZipException { + final Path parentFolderPath = fileToWritePath.getParent(); + if (parentFolderPath != null) { + try { + Files.createDirectories(parentFolderPath); + } catch (final IOException e) { + throw new ZipException( + String.format("Could not create parent directories of '%s'", fileToWritePath.toString()), e); + } + } + try (final FileOutputStream outputStream = new FileOutputStream(fileToWritePath.toFile())) { + IOUtils.copy(zipInputStream, outputStream); + } catch (final FileNotFoundException e) { + throw new ZipException(String.format("Could not find file '%s'", fileToWritePath.toString()), e); + } catch (final IOException e) { + throw new ZipException( + String.format("An unexpected error has occurred while writing file '%s'", fileToWritePath.toString()) + , e); + } + } + + /** + * Creates the path directories if the provided path does not exists. + * + * @param path the path to create directories + * @throws ZipException when there was a problem to create the directories + */ + private static void createDirectoryIfNotExists(final Path path) throws ZipException { + if(path.toFile().exists()) { + return; + } + try { + Files.createDirectories(path); + } catch (final IOException e) { + throw new ZipException(String.format("Could not create directories for path '%s'", path.toString()), e); + } + } + + /** + * Zips a directory and its children content. + * + * @param fromPath the directory path to zip + * @param toZipFilePath the path to the zip file that will be created + * @throws ZipException when there was a problem during the zip process + */ + public static void createZipFromPath(final Path fromPath, final Path toZipFilePath) throws ZipException { + final Path createdZipFilePath; + try { + createdZipFilePath = Files.createFile(toZipFilePath); + } catch (final IOException e) { + throw new ZipException(String.format("Could not create file '%s'", toZipFilePath.toString()), e); + } + + try(final FileOutputStream fileOutputStream = new FileOutputStream(createdZipFilePath.toFile()); + final BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream); + final ZipOutputStream zipOut = new ZipOutputStream(bos); + final Stream<Path> walkStream = Files.walk(fromPath)) { + final Set<Path> allFilesSet = walkStream.collect(Collectors.toSet()); + for (final Path path : allFilesSet) { + checkForZipSlipInRead(path); + if (path.equals(fromPath)) { + continue; + } + final Path relativePath = fromPath.relativize(path); + final File file = path.toFile(); + if (file.isDirectory()) { + zipOut.putNextEntry(new ZipEntry(relativePath.toString() + File.separator)); + } else { + zipOut.putNextEntry(new ZipEntry(relativePath.toString())); + zipOut.write(Files.readAllBytes(path)); + } + zipOut.closeEntry(); + } + } catch (final FileNotFoundException e) { + throw new ZipException(String.format("Could not create file '%s'", toZipFilePath.toString()), e); + } catch (final IOException e) { + throw new ZipException("An error has occurred while creating the zip package", e); + } + } + +} diff --git a/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipException.java b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipException.java new file mode 100644 index 0000000000..414b4823ad --- /dev/null +++ b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipException.java @@ -0,0 +1,31 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.common.zip.exception; + +public class ZipException extends Exception { + + public ZipException(String s) { + super(s); + } + + public ZipException(String s, Throwable throwable) { + super(s, throwable); + } +} diff --git a/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipSlipException.java b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipSlipException.java new file mode 100644 index 0000000000..203e357f8f --- /dev/null +++ b/common-app-api/src/main/java/org/openecomp/sdc/common/zip/exception/ZipSlipException.java @@ -0,0 +1,27 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.common.zip.exception; + +public class ZipSlipException extends ZipException { + + public ZipSlipException(final String filePath) { + super(String.format("Zip slip attempt detected in file: %s", filePath)); + } +} diff --git a/common-app-api/src/test/java/org/openecomp/sdc/common/util/ZipUtilTest.java b/common-app-api/src/test/java/org/openecomp/sdc/common/util/ZipUtilTest.java deleted file mode 100644 index 71e8d1509a..0000000000 --- a/common-app-api/src/test/java/org/openecomp/sdc/common/util/ZipUtilTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.openecomp.sdc.common.util; - -import org.junit.Test; - -import java.util.Map; - -public class ZipUtilTest { - - // private ZipUtil createTestSubject() { - // return new ZipUtil(); - // } - - @Test - public void testReadZip() throws Exception { - byte[] zipAsBytes = new byte[] { ' ' }; - Map<String, byte[]> result; - - // default test - result = ZipUtil.readZip(zipAsBytes); - } - - @Test - public void testMain() throws Exception { - String[] args = new String[] { "" }; - - // default test - ZipUtil.main(args); - } - - @Test - public void testZipBytes() throws Exception { - byte[] input = new byte[] { ' ' }; - byte[] result; - - // default test - result = ZipUtil.zipBytes(input); - } - - @Test - public void testUnzip() throws Exception { - byte[] zipped = new byte[] { ' ' }; - byte[] result; - - // default test - result = ZipUtil.unzip(zipped); - } -} diff --git a/common-app-api/src/test/java/org/openecomp/sdc/common/zip/ZipUtilsTest.java b/common-app-api/src/test/java/org/openecomp/sdc/common/zip/ZipUtilsTest.java new file mode 100644 index 0000000000..d5fb3dcc8b --- /dev/null +++ b/common-app-api/src/test/java/org/openecomp/sdc/common/zip/ZipUtilsTest.java @@ -0,0 +1,172 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.common.zip; + +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isIn; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.openecomp.sdc.common.zip.exception.ZipException; +import org.openecomp.sdc.common.zip.exception.ZipSlipException; + +public class ZipUtilsTest { + + @Test + public void testZipSlipInRead() { + final byte[] windowsZipBytes; + final byte[] linuxZipBytes; + try { + final InputStream linuxZipAsStream = ZipUtilsTest.class.getClassLoader().getResourceAsStream("zip-slip/zip-slip-linux.zip"); + final InputStream windowsZipAsStream = ZipUtilsTest.class.getClassLoader().getResourceAsStream("zip-slip/zip-slip-windows.zip"); + if(linuxZipAsStream == null || windowsZipAsStream == null) { + fail("Could not load the zip slip files"); + } + linuxZipBytes = IOUtils.toByteArray(linuxZipAsStream); + windowsZipBytes = IOUtils.toByteArray(windowsZipAsStream); + } catch (final IOException e) { + e.printStackTrace(); + fail("Could not load the required zip slip files"); + return; + } + + try { + ZipUtils.readZip(linuxZipBytes, true); + fail("Zip slip should be detected"); + } catch (final ZipException ex) { + assertThat("Expected ZipSlipException", ex, is(instanceOf(ZipSlipException.class))); + } + + try { + ZipUtils.readZip(windowsZipBytes, true); + fail("Zip slip should be detected"); + } catch (final ZipException ex) { + assertThat("Expected ZipSlipException", ex, is(instanceOf(ZipSlipException.class))); + } + } + + @Test + public void testZipSlipInUnzip() throws IOException { + final Path tempDirectoryWindows = Files.createTempDirectory("zipSlipWindows" + System.currentTimeMillis()); + final Path tempDirectoryLinux = Files.createTempDirectory("zipSlipLinux" + System.currentTimeMillis()); + try { + final Path linuxZipPath; + final Path windowsZipPath; + try { + linuxZipPath = Paths + .get(ZipUtilsTest.class.getClassLoader().getResource("zip-slip/zip-slip-linux.zip").toURI()); + windowsZipPath = Paths + .get(ZipUtilsTest.class.getClassLoader().getResource("zip-slip/zip-slip-windows.zip").toURI()); + } catch (final URISyntaxException e) { + fail("Could not load the required zip slip files"); + return; + } + + try { + ZipUtils.unzip(windowsZipPath, tempDirectoryWindows); + ZipUtils.unzip(linuxZipPath, tempDirectoryLinux); + fail("Zip slip should be detected"); + } catch (final ZipException ex) { + assertThat("At least one of the zip files should throw ZipSlipException", + ex, is(instanceOf(ZipSlipException.class))); + } + } finally { + org.apache.commons.io.FileUtils.deleteDirectory(tempDirectoryLinux.toFile()); + org.apache.commons.io.FileUtils.deleteDirectory(tempDirectoryWindows.toFile()); + } + } + + @Test + public void testUnzipAndZip() throws IOException, ZipException { + final Path unzipTempPath = Files.createTempDirectory("testUnzip"); + final Path zipTempPath = Files.createTempDirectory("testZip"); + final Path testZipPath; + try { + try { + testZipPath = Paths + .get(ZipUtilsTest.class.getClassLoader().getResource("zip/extract-test.zip").toURI()); + ZipUtils.unzip(testZipPath, unzipTempPath); + } catch (final URISyntaxException e) { + fail("Could not load the required zip file"); + return; + } + + final Set<Path> expectedPaths = new HashSet<>(); + expectedPaths.add(Paths.get(unzipTempPath.toString(),"rootFile1.txt")); + expectedPaths.add(Paths.get(unzipTempPath.toString(),"rootFileNoExtension")); + expectedPaths.add(Paths.get(unzipTempPath.toString(),"EmptyFolder")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "SingleLvlFolder")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "SingleLvlFolder", "singleLvlFolderFile.txt")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "SingleLvlFolder", "singleLvlFolderFileNoExtension")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder", "twoLvlFolderFile.txt")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder", "twoLvlFolderFileNoExtension")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder", "SingleLvlFolder")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder", "SingleLvlFolder", "singleLvlFolderFile.txt")); + expectedPaths.add(Paths.get(unzipTempPath.toString(), "TwoLvlFolder", "SingleLvlFolder", "singleLvlFolderFileNoExtension")); + + final AtomicLong actualPathCount = new AtomicLong(0); + try (Stream<Path> stream = Files.walk(unzipTempPath)) { + stream.filter(path -> !unzipTempPath.equals(path)).forEach(actualPath -> { + actualPathCount.getAndIncrement(); + assertThat("Unzipped file should be in the expected list", actualPath, isIn(expectedPaths)); + }); + } + assertThat("The number of unzipped files should be as expected", actualPathCount.get(), is((long) expectedPaths.size())); + final Path zipFilePath = zipTempPath.resolve("testzip.zip"); + ZipUtils.createZipFromPath(unzipTempPath, zipFilePath); + final Map<String, byte[]> fileMap = ZipUtils.readZip(zipFilePath.toFile(), true); + //matching the folder pattern of the readZip + final Set<String> expectedPathStringSet = expectedPaths.stream() + .map(path -> { + final Path relativePath = unzipTempPath.relativize(path); + return path.toFile().isDirectory() ? relativePath.toString() + File.separator : relativePath.toString(); + }).collect(Collectors.toSet()); + assertThat("The number of zipped files should be as expected", fileMap, aMapWithSize(expectedPathStringSet.size())); + fileMap.keySet().forEach(s -> { + assertThat("File in zip package should be in the expected list", s, isIn(expectedPathStringSet)); + }); + } finally { + FileUtils.deleteDirectory(unzipTempPath.toFile()); + FileUtils.deleteDirectory(zipTempPath.toFile()); + } + } + + + +}
\ No newline at end of file diff --git a/common-app-api/src/test/resources/zip-slip/zip-slip-linux.zip b/common-app-api/src/test/resources/zip-slip/zip-slip-linux.zip Binary files differnew file mode 100644 index 0000000000..38b3f499de --- /dev/null +++ b/common-app-api/src/test/resources/zip-slip/zip-slip-linux.zip diff --git a/common-app-api/src/test/resources/zip-slip/zip-slip-windows.zip b/common-app-api/src/test/resources/zip-slip/zip-slip-windows.zip Binary files differnew file mode 100644 index 0000000000..3474c88bec --- /dev/null +++ b/common-app-api/src/test/resources/zip-slip/zip-slip-windows.zip diff --git a/common-app-api/src/test/resources/zip/extract-test.zip b/common-app-api/src/test/resources/zip/extract-test.zip Binary files differnew file mode 100644 index 0000000000..880452fdc7 --- /dev/null +++ b/common-app-api/src/test/resources/zip/extract-test.zip |