CVE-2021-39180: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: OpenOLAT is a web-based learning management system (LMS). A path traversal vulnerability exists in versions prior to 15.3.18, 15.5.3, and 16.0.0. Using a specially prepared ZIP file, it is possible to overwrite any file that is writable by the application server user (e.g. the tomcat user). Depending on the configuration this can be limited to files of the OpenOlat user data directory, however, if not properly set up, the attack could also be used to overwrite application server config files, java code or even operating system files. The attack could be used to corrupt or modify any OpenOlat file such as course structures, config files or temporary test data. Those attack would require in-depth knowledge of the installation and thus more theoretical. If the app server configuration allows the execution of jsp files and the path to the context is known, it is also possible to execute java code. If the app server runs with the same user that is used to deploy the OpenOlat code or has write permissions on the OpenOlat code files and the path to the context is know, code injection is possible. The attack requires an OpenOlat user account to upload a ZIP file and trigger the unzip method. It can not be exploited by unregistered users. The problem is fixed in versions 15.3.18, 15.5.3 and 16.0.0. There are no known workarounds aside from upgrading.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 8.8
Published: 2021-08-31T18:15Z
Repository: https://github.com/OpenOLAT/OpenOLAT
Language: Java
Files changed: src/main/java/org/olat/core/util/ZipUtil.java, src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java, src/main/java/org/olat/core/util/vfs/MergeSource.java, src/main/java/org/olat/core/util/vfs/NamedContainerImpl.java, src/main/java/org/olat/core/util/vfs/VFSContainer.java, src/main/java/org/olat/core/util/vfs/VirtualContainer.java, src/main/java/org/olat/course/assessment/bulk/DataStepForm.java, src/main/java/org/olat/ims/cp/ui/VFSCPContainer.java, src/main/java/org/olat/ims/cp/ui/VFSCPNamedContainerItem.java, src/main/java/org/olat/ims/cp/ui/VFSMediaFilesContainer.java, src/main/java/org/olat/ims/cp/ui/VFSRootCPContainer.java, src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java, src/main/java/org/olat/ims/qti21/repository/handlers/CopyAndConvertVisitor.java, src/main/java/org/olat/modules/wiki/WikiManager.java, src/test/java/org/olat/core/util/ZipUtilTest.java, src/test/java/org/olat/test/AllTestsJunit4.java
Commit date: 2021-06-21 16:32:21 +0200
Commit message: OO-5549: check parent by unzip
Diff: diff --git a/src/main/java/org/olat/core/util/ZipUtil.java b/src/main/java/org/olat/core/util/ZipUtil.java
index c04a98aef0..e08bbd730a 100644
--- a/src/main/java/org/olat/core/util/ZipUtil.java
+++ b/src/main/java/org/olat/core/util/ZipUtil.java
@@ -35,10 +35,11 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -186,24 +187,28 @@ public class ZipUtil {
 	 * @return	True if successful, false otherwise
 	 */
 	private static boolean unzip(InputStream in, VFSContainer targetDir, Identity identity, boolean versioning) {
 		
 		VFSRepositoryService vfsRepositoryService = CoreSpringFactory.getImpl(VFSRepositoryService.class);
-		
+
 		try(ZipInputStream oZip = new ZipInputStream(in)) {
 			// unzip files
 			ZipEntry oEntr = oZip.getNextEntry();
 			while (oEntr != null) {
-				if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) {
+				String name = oEntr.getName();
+				if(!targetDir.isInPath(name)) {
+					throw new IOException("Invalip ZIP");
+				}
+
+				if (name != null && !name.startsWith(DIR_NAME__MACOSX)) {
 					if (oEntr.isDirectory()) {
 						// skip MacOSX specific metadata directory
 						// create directories
-						getAllSubdirs(targetDir, oEntr.getName(), identity, true);
+						getAllSubdirs(targetDir, name, identity, true);
 					} else {
 						// create file
 						VFSContainer createIn = targetDir;
-						String name = oEntr.getName();
 						// check if entry has directories which did not show up as
 						// directories above
 						int dirSepIndex = name.lastIndexOf('/');
 						if (dirSepIndex == -1) {
 							// try it windows style, backslash is also valid format
@@ -315,11 +320,11 @@ public class ZipUtil {
 	private static boolean unzipNonStrict(InputStream in, VFSContainer targetDir, Identity identity, boolean versioning) {
 		try(net.sf.jazzlib.ZipInputStream oZip = new net.sf.jazzlib.ZipInputStream(in)) {
 			VFSRepositoryService vfsRepositoryService = CoreSpringFactory.getImpl(VFSRepositoryService.class);
 			
 			// unzip files
-			net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry();
+			net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry();//TODO zip
 			
 			VFSLeaf lastLeaf = null;
 			while (oEntr != null) {
 				if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) {
 					if (oEntr.isDirectory()) {
@@ -453,11 +458,11 @@ public class ZipUtil {
 				if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) {
 					if (oEntr.isDirectory()) {
 						// skip MacOSX specific metadata directory
 						// directories aren't locked
 						oZip.closeEntry();
-						oEntr = oZip.getNextEntry();
+						oEntr = oZip.getNextEntry();//TODO zip
 						continue;
 					} else {
 						// search file
 						VFSContainer createIn = targetDir;
 						String name = oEntr.getName();
@@ -845,13 +850,21 @@ public class ZipUtil {
 	 * 
 	 * @param is, stream from zip archive
 	 * @param outdir, path to output directory, relative to cwd or absolute
 	 */
 	private static void xxunzip(InputStream is, String outdir) throws IOException {
+		final Path outPath = Paths.get(outdir);
+		
 		try(ZipInputStream zis = new ZipInputStream (new BufferedInputStream(is))) {
 			ZipEntry entry;
 			while ((entry = zis.getNextEntry()) != null) {
+				Path filePath = Paths.get(outdir, entry.getName());
+				Path normalizedPath = filePath.normalize();
+				if(!normalizedPath.startsWith(outPath)) {
+					throw new IOException("Invalid ZIP");
+				}
+				
 				File of = new File(outdir, entry.getName());
 				if (entry.isDirectory()) {
 					of.mkdirs();
 				} else {
 					File parent = of.getParentFile();
diff --git a/src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java b/src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java
index 085e878ef1..9504e412fa 100644
--- a/src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java
+++ b/src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java
@@ -333,10 +333,18 @@ public class LocalFolderImpl extends LocalImpl implements VFSContainer {
 			resolvedContainer.setDefaultItemFilter(defaultFilter);
 		}
 		return resolved;
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		Path bFile = getBasefile().toPath();
+		Path filePath = bFile.resolve(path);
+		Path normalizedPath = filePath.normalize();
+		return normalizedPath.startsWith(bFile);
+	}
+
 	@Override
 	public String getRelPath() {
 		Path bFile = getBasefile().toPath();
 		Path bcRoot = FolderConfig.getCanonicalRootPath();
 		
diff --git a/src/main/java/org/olat/core/util/vfs/MergeSource.java b/src/main/java/org/olat/core/util/vfs/MergeSource.java
index 6bfc7b4def..17600068fc 100644
--- a/src/main/java/org/olat/core/util/vfs/MergeSource.java
+++ b/src/main/java/org/olat/core/util/vfs/MergeSource.java
@@ -200,10 +200,15 @@ public class MergeSource extends AbstractVirtualContainer {
 	public VFSLeaf createChildLeaf(String name) {
 		if (canWrite() != VFSConstants.YES) return null;
 		return rootWriteContainer.createChildLeaf(name);
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return rootWriteContainer.isInPath(path);
+	}
+
 	@Override
 	public VFSStatus copyFrom(VFSItem source) {
 		if (canWrite() != VFSConstants.YES) {
 			throw new AssertException("Cannot create child container in merge source if not writable.");
 		}
diff --git a/src/main/java/org/olat/core/util/vfs/NamedContainerImpl.java b/src/main/java/org/olat/core/util/vfs/NamedContainerImpl.java
index 036ed3e4b2..93a4e814db 100644
--- a/src/main/java/org/olat/core/util/vfs/NamedContainerImpl.java
+++ b/src/main/java/org/olat/core/util/vfs/NamedContainerImpl.java
@@ -119,10 +119,15 @@ public class NamedContainerImpl extends AbstractVirtualContainer {
 	@Override
 	public String getRelPath() {
 		return getDelegate().getRelPath();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return getDelegate().isInPath(path);
+	}
+
 	@Override
 	public VFSStatus delete() {
 		return getDelegate().delete();
 	}
 
diff --git a/src/main/java/org/olat/core/util/vfs/VFSContainer.java b/src/main/java/org/olat/core/util/vfs/VFSContainer.java
index c1ddfb6657..6a91206b9b 100644
--- a/src/main/java/org/olat/core/util/vfs/VFSContainer.java
+++ b/src/main/java/org/olat/core/util/vfs/VFSContainer.java
@@ -84,10 +84,17 @@ public interface VFSContainer extends VFSItem {
 	 * @param name
 	 * @return VFSItem if successfull, null otherwise.
 	 */
 	public VFSLeaf createChildLeaf(String name);
 	
+	/**
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public boolean isInPath(String path);
+	
 	/**
 	 * Set a default filter that will be applied to this container getItems method
 	 * 
 	 * @param defaultFilter
 	 */
diff --git a/src/main/java/org/olat/core/util/vfs/VirtualContainer.java b/src/main/java/org/olat/core/util/vfs/VirtualContainer.java
index 53e338f9ce..44f499b93b 100644
--- a/src/main/java/org/olat/core/util/vfs/VirtualContainer.java
+++ b/src/main/java/org/olat/core/util/vfs/VirtualContainer.java
@@ -103,10 +103,15 @@ public class VirtualContainer extends AbstractVirtualContainer {
 	@Override
 	public String getRelPath() {
 		return null;
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
+
 	@Override
 	public VFSItem resolve(String path) {
 		if(path != null && path.length() > 1 && path.startsWith("/")) {
 			String childName = VFSManager.extractChild(path);
 			String nextPath = path.substring(childName.length() + 1);
diff --git a/src/main/java/org/olat/course/assessment/bulk/DataStepForm.java b/src/main/java/org/olat/course/assessment/bulk/DataStepForm.java
index 98209eb667..33664e17fe 100644
--- a/src/main/java/org/olat/course/assessment/bulk/DataStepForm.java
+++ b/src/main/java/org/olat/course/assessment/bulk/DataStepForm.java
@@ -364,11 +364,11 @@ public class DataStepForm extends StepFormBasicController {
 
 			ZipEntry entry;
 			try(InputStream is = target.getInputStream();
 					ZipInputStream zis = new ZipInputStream(is)) {
 				byte[] b = new byte[FileUtils.BSIZE];
-				while ((entry = zis.getNextEntry()) != null) {
+				while ((entry = zis.getNextEntry()) != null) {//TODO zip
 					if(!entry.isDirectory()) {
 						while (zis.read(b) > 0) {
 							//continue
 						}
 
diff --git a/src/main/java/org/olat/ims/cp/ui/VFSCPContainer.java b/src/main/java/org/olat/ims/cp/ui/VFSCPContainer.java
index faad708dde..8a387ad7d8 100644
--- a/src/main/java/org/olat/ims/cp/ui/VFSCPContainer.java
+++ b/src/main/java/org/olat/ims/cp/ui/VFSCPContainer.java
@@ -94,10 +94,15 @@ public class VFSCPContainer extends AbstractVirtualContainer implements VFSConta
 
 	@Override
 	public String getRelPath() {
 		return null;
 	}
+	
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
 
 	@Override
 	public List<VFSItem> getItems() {
 		return getItems(cp, treeModel, rootNodeId);
 	}
diff --git a/src/main/java/org/olat/ims/cp/ui/VFSCPNamedContainerItem.java b/src/main/java/org/olat/ims/cp/ui/VFSCPNamedContainerItem.java
index 2ebeb4d049..9da2cdf8ac 100644
--- a/src/main/java/org/olat/ims/cp/ui/VFSCPNamedContainerItem.java
+++ b/src/main/java/org/olat/ims/cp/ui/VFSCPNamedContainerItem.java
@@ -69,10 +69,15 @@ public class VFSCPNamedContainerItem extends NamedLeaf implements VFSContainer {
 	@Override
 	public List<VFSItem> getItems(VFSItemFilter filter) {
 		return getItems();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
+
 	@Override
 	public VFSStatus copyFrom(VFSItem source) {
 		return VFSConstants.NO;
 	}
 
diff --git a/src/main/java/org/olat/ims/cp/ui/VFSMediaFilesContainer.java b/src/main/java/org/olat/ims/cp/ui/VFSMediaFilesContainer.java
index 298da4ab63..2718f8f675 100644
--- a/src/main/java/org/olat/ims/cp/ui/VFSMediaFilesContainer.java
+++ b/src/main/java/org/olat/ims/cp/ui/VFSMediaFilesContainer.java
@@ -83,10 +83,15 @@ public class VFSMediaFilesContainer extends AbstractVirtualContainer implements
 	@Override
 	public String getRelPath() {
 		return rootContainer.getRelPath();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return rootContainer.isInPath(path);
+	}
+
 	@Override
 	public VFSContainer getParentContainer() {
 		return null;
 	}
 
diff --git a/src/main/java/org/olat/ims/cp/ui/VFSRootCPContainer.java b/src/main/java/org/olat/ims/cp/ui/VFSRootCPContainer.java
index 8d846846ca..c03fa08bb2 100644
--- a/src/main/java/org/olat/ims/cp/ui/VFSRootCPContainer.java
+++ b/src/main/java/org/olat/ims/cp/ui/VFSRootCPContainer.java
@@ -110,10 +110,15 @@ public class VFSRootCPContainer extends AbstractVirtualContainer implements VFSC
 	@Override
 	public String getRelPath() {
 		return rootContainer.getRelPath();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return rootContainer.isInPath(path);
+	}
+
 	@Override
 	public VFSItem resolve(String path) {
 		// 1) try to resolve directly from root (HTML editor instance)
 		VFSItem item = rootContainer.resolve(path);
 		if (item != null) {
diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
index 16e008a2ba..5138f8a29d 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
@@ -159,10 +159,15 @@ public class QTI21ImportProcessor {
 			Path parentPath = imsmanifestPath.getParent();
 			Path assessmentItemPath = parentPath.resolve(href);
 			if(Files.notExists(assessmentItemPath)) {
 				return null;
 			}
+			
+			Path normalizedPath = assessmentItemPath.normalize();
+			if(!normalizedPath.startsWith(parentPath)) {
+				throw new IOException("Invalid Item");
+			}
 	
 			String dir = qpoolFileStorage.generateDir();
 			//storage
 			File itemStorage = qpoolFileStorage.getDirectory(dir);
 			File outputFile = new File(itemStorage, href);
@@ -211,10 +216,14 @@ public class QTI21ImportProcessor {
 			//process material
 			List<String> materials = ImportExportHelper.getMaterials(assessmentItem);
 			for(String material:materials) {
 				if(material.indexOf("://") < 0) {// material can be an external URL
 					Path materialFile = assessmentItemPath.getParent().resolve(material);
+					Path normalizedMaterialPath = materialFile.normalize();
+					if(!normalizedMaterialPath.startsWith(parentPath)) {
+						throw new IOException("Invalid Item");
+					}
 					PathUtils.copyFileToDir(materialFile, outputFile.getParentFile(), material);
 				}
 			}
 			return qitem;
 		} catch (Exception e) {
diff --git a/src/main/java/org/olat/ims/qti21/repository/handlers/CopyAndConvertVisitor.java b/src/main/java/org/olat/ims/qti21/repository/handlers/CopyAndConvertVisitor.java
index 7e837f6d0f..0aec03bd9c 100644
--- a/src/main/java/org/olat/ims/qti21/repository/handlers/CopyAndConvertVisitor.java
+++ b/src/main/java/org/olat/ims/qti21/repository/handlers/CopyAndConvertVisitor.java
@@ -113,29 +113,40 @@ class CopyAndConvertVisitor extends SimpleFileVisitor<Path> {
 		if(filter.matches(file)) {
 			String filename = file.getFileName().toString();
 			if(filename.startsWith(".")) {
 				//ignore
 			} else if(filename.endsWith("xml") && !filename.equals("imsmanifest.xml")) {
+				checkPath(destFile);
 				convertXmlFile(file, destFile);
 			} else {
+				checkPath(destFile);
 				Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
 			}
 		}
         return FileVisitResult.CONTINUE;
 	}
  
 	@Override
 	public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
 	throws IOException {
 		Path relativeDir = source.relativize(dir);
-        final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString());
+		final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString());
+		checkPath(dirToCreate);
+       
         if(!dirToCreate.toFile().exists()) {
         	Files.createDirectory(dirToCreate);
         }
         return FileVisitResult.CONTINUE;
 	}
 	
+	private void checkPath(Path file) throws IOException {
+		Path normalizedPath = file.normalize();
+		if(!normalizedPath.startsWith(destDir)) {
+			throw new IOException("Invalid ZIP");
+		}
+	}
+	
 	/**
 	 * Convert the XML files, assessmentItem or assessmentTest
 	 * 
 	 * @param inputFile
 	 * @param outputFile
diff --git a/src/main/java/org/olat/modules/wiki/WikiManager.java b/src/main/java/org/olat/modules/wiki/WikiManager.java
index 208a012955..4473c5b248 100644
--- a/src/main/java/org/olat/modules/wiki/WikiManager.java
+++ b/src/main/java/org/olat/modules/wiki/WikiManager.java
@@ -296,19 +296,24 @@ public class WikiManager {
 		}
 		
 		@Override
 		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 	    throws IOException {
-	        String filename = file.getFileName().toString();
+			String filename = file.getFileName().toString();
+			Path normalizedPath = file.normalize();
+			if(!normalizedPath.startsWith(destDir)) {
+				throw new IOException("Invalid ZIP");
+			}
+			
 	        if(filename.endsWith(WikiManager.WIKI_PROPERTIES_SUFFIX)) {
-	        		String f = convertAlternativeFilename(file.toString());
-	        		final Path destFile = Paths.get(wikiDir.toString(), f);
-	        		resetAndCopyProperties(file, destFile);
+	        	String f = convertAlternativeFilename(file.toString());
+	        	final Path destFile = Paths.get(wikiDir.toString(), f);
+	        	resetAndCopyProperties(file, destFile);
 	        } else if (filename.endsWith(WIKI_FILE_SUFFIX)) {
-        			String f = convertAlternativeFilename(file.toString());
-	        		final Path destFile = Paths.get(wikiDir.toString(), f);
-				Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
+	        	String f = convertAlternativeFilename(file.toString());
+	        	final Path destFile = Paths.get(wikiDir.toString(), f);
+	        	Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
 			} else if (!filename.contains(WIKI_FILE_SUFFIX + "-")
 					&& !filename.contains(WIKI_PROPERTIES_SUFFIX + "-")) {
 				final Path destFile = Paths.get(mediaDir.toString(), file.toString());
 				Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
 			}
diff --git a/src/test/java/org/olat/core/util/Slide.zip b/src/test/java/org/olat/core/util/Slide.zip
new file mode 100644
index 0000000000..e72f002668
Binary files /dev/null and b/src/test/java/org/olat/core/util/Slide.zip differ
diff --git a/src/test/java/org/olat/core/util/ZipUtilTest.java b/src/test/java/org/olat/core/util/ZipUtilTest.java
new file mode 100644
index 0000000000..6d82928af4
--- /dev/null
+++ b/src/test/java/org/olat/core/util/ZipUtilTest.java
@@ -0,0 +1,76 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util;
+
+import java.io.File;
+import java.net.URL;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.commons.modules.bc.FolderConfig;
+import org.olat.core.id.Identity;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.core.util.vfs.VFSItem;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.core.util.vfs.VFSManager;
+import org.olat.core.util.vfs.callbacks.FullAccessCallback;
+import org.olat.test.OlatTestCase;
+import org.olat.test.VFSJavaIOFile;
+
+/**
+ * 
+ * Initial date: 21 juin 2021<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ZipUtilTest extends OlatTestCase {
+
+	@Test
+	public void unzipFileToDir() throws Exception {
+		File tmpFolder = new File(WebappHelper.getTmpDir(), "zip" + CodeHelper.getRAMUniqueID());
+		URL url = ZipUtilTest.class.getResource("Slide.zip");
+		File zipFile = new File(url.toURI());
+		
+		File unzipDir = new File(tmpFolder, "unzip1");
+		unzipDir.mkdirs();
+		
+		boolean success = ZipUtil.unzip(zipFile, unzipDir);
+		Assert.assertFalse(success);
+		File img = new File(tmpFolder, "IMG_1489.png");
+		Assert.assertFalse(img.exists());
+		FileUtils.deleteDirsAndFiles(tmpFolder, true, true);
+	}
+	
+	@Test
+	public void unzipLeafToContainer() throws Exception {
+		URL url = ZipUtilTest.class.getResource("Slide.zip");
+		VFSLeaf zipLeaf = new VFSJavaIOFile(url.toURI());
+		VFSContainer tmpDir = VFSManager.olatRootContainer(FolderConfig.getRelativeTmpDir());
+		tmpDir.setLocalSecurityCallback(new FullAccessCallback());
+		VFSContainer targetDir = tmpDir.createChildContainer("zip" + CodeHelper.getForeverUniqueID());
+		boolean success = ZipUtil.unzip(zipLeaf, targetDir, (Identity)null, false);
+		
+		VFSItem img = tmpDir.resolve("IMG_1489.png");
+		Assert.assertNull(img);
+		Assert.assertFalse(success);
+		
+		targetDir.deleteSilently();
+	}
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 12bd35915a..031601a937 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -71,10 +71,11 @@ import org.junit.runners.Suite;
 	org.olat.core.util.FormatterHourAndSecondsTest.class,
 	org.olat.core.util.EncoderTest.class,
 	org.olat.core.util.SimpleHtmlParserTest.class,
 	org.olat.core.util.IPUtilsTest.class,
 	org.olat.core.util.IPUtilsValidRangeTest.class,
+	org.olat.core.util.ZipUtilTest.class,
 	org.olat.core.util.ZipUtilConcatTest.class,
 	org.olat.core.util.mail.EmailAddressValidatorTest.class,
 	org.olat.core.util.mail.manager.MailManagerTest.class,
 	org.olat.core.util.mail.manager.MailUserDataManagerTest.class,
 	org.olat.core.util.openxml.OpenXmlWorkbookTest.class,

Vulnerable code: 		
				if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) {
						getAllSubdirs(targetDir, oEntr.getName(), identity, true);
						String name = oEntr.getName();
			net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry();
						oEntr = oZip.getNextEntry();
				while ((entry = zis.getNextEntry()) != null) {
        final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString());
	        String filename = file.getFileName().toString();
	        		String f = convertAlternativeFilename(file.toString());
	        		final Path destFile = Paths.get(wikiDir.toString(), f);
	        		resetAndCopyProperties(file, destFile);
        			String f = convertAlternativeFilename(file.toString());
	        		final Path destFile = Paths.get(wikiDir.toString(), f);
				Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
Fixed code: import java.nio.file.Paths;

				String name = oEntr.getName();
				if(!targetDir.isInPath(name)) {
					throw new IOException("Invalip ZIP");
				}

				if (name != null && !name.startsWith(DIR_NAME__MACOSX)) {
						getAllSubdirs(targetDir, name, identity, true);
			net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry();//TODO zip
						oEntr = oZip.getNextEntry();//TODO zip
		final Path outPath = Paths.get(outdir);
		
				Path filePath = Paths.get(outdir, entry.getName());
				Path normalizedPath = filePath.normalize();
				if(!normalizedPath.startsWith(outPath)) {
					throw new IOException("Invalid ZIP");
				}
				
	@Override
	public boolean isInPath(String path) {
		Path bFile = getBasefile().toPath();
		Path filePath = bFile.resolve(path);
		Path normalizedPath = filePath.normalize();
		return normalizedPath.startsWith(bFile);
	}

	@Override
	public boolean isInPath(String path) {
		return rootWriteContainer.isInPath(path);
	}

	@Override
	public boolean isInPath(String path) {
		return getDelegate().isInPath(path);
	}

	/**
	 * 
	 * @param path
	 * @return
	 */
	public boolean isInPath(String path);
	
	@Override
	public boolean isInPath(String path) {
		return false;
	}

				while ((entry = zis.getNextEntry()) != null) {//TODO zip
	
	@Override
	public boolean isInPath(String path) {
		return false;
	}
	@Override
	public boolean isInPath(String path) {
		return false;
	}

	@Override
	public boolean isInPath(String path) {
		return rootContainer.isInPath(path);
	}

	@Override
	public boolean isInPath(String path) {
		return rootContainer.isInPath(path);
	}

			
			Path normalizedPath = assessmentItemPath.normalize();
			if(!normalizedPath.startsWith(parentPath)) {
				throw new IOException("Invalid Item");
			}
					Path normalizedMaterialPath = materialFile.normalize();
					if(!normalizedMaterialPath.startsWith(parentPath)) {
						throw new IOException("Invalid Item");
					}
				checkPath(destFile);
				checkPath(destFile);
		final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString());
		checkPath(dirToCreate);
       
	private void checkPath(Path file) throws IOException {
		Path normalizedPath = file.normalize();
		if(!normalizedPath.startsWith(destDir)) {
			throw new IOException("Invalid ZIP");
		}
	}
	
			String filename = file.getFileName().toString();
			Path normalizedPath = file.normalize();
			if(!normalizedPath.startsWith(destDir)) {
				throw new IOException("Invalid ZIP");
			}
			
	        	String f = convertAlternativeFilename(file.toString());
	        	final Path destFile = Paths.get(wikiDir.toString(), f);
	        	resetAndCopyProperties(file, destFile);
	        	String f = convertAlternativeFilename(file.toString());
	        	final Path destFile = Paths.get(wikiDir.toString(), f);
	        	Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
/**
 * <a href="http://www.openolat.org">
 * OpenOLAT - Online Learning and Training</a><br>
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); <br>
 * you may not use this file except in compliance with the License.<br>
 * You may obtain a copy of the License at the
 * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
 * <p>
 * Unless required by applicable law or agreed to in writing,<br>
 * software distributed under the License is distributed on an "AS IS" BASIS, <br>
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
 * See the License for the specific language governing permissions and <br>
 * limitations under the License.
 * <p>
 * Initial code contributed and copyrighted by<br>
 * frentix GmbH, http://www.frentix.com
 * <p>
 */
package org.olat.core.util;

import java.io.File;
import java.net.URL;

import org.junit.Assert;
import org.junit.Test;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.id.Identity;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.callbacks.FullAccessCallback;
import org.olat.test.OlatTestCase;
import org.olat.test.VFSJavaIOFile;

/**
 * 
 * Initial date: 21 juin 2021<br>
 * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
 *
 */
public class ZipUtilTest extends OlatTestCase {

	@Test
	public void unzipFileToDir() throws Exception {
		File tmpFolder = new File(WebappHelper.getTmpDir(), "zip" + CodeHelper.getRAMUniqueID());
		URL url = ZipUtilTest.class.getResource("Slide.zip");
		File zipFile = new File(url.toURI());
		
		File unzipDir = new File(tmpFolder, "unzip1");
		unzipDir.mkdirs();
		
		boolean success = ZipUtil.unzip(zipFile, unzipDir);
		Assert.assertFalse(success);
		File img = new File(tmpFolder, "IMG_1489.png");
		Assert.assertFalse(img.exists());
		FileUtils.deleteDirsAndFiles(tmpFolder, true, true);
	}
	
	@Test
	public void unzipLeafToContainer() throws Exception {
		URL url = ZipUtilTest.class.getResource("Slide.zip");
		VFSLeaf zipLeaf = new VFSJavaIOFile(url.toURI());
		VFSContainer tmpDir = VFSManager.olatRootContainer(FolderConfig.getRelativeTmpDir());
		tmpDir.setLocalSecurityCallback(new FullAccessCallback());
		VFSContainer targetDir = tmpDir.createChildContainer("zip" + CodeHelper.getForeverUniqueID());
		boolean success = ZipUtil.unzip(zipLeaf, targetDir, (Identity)null, false);
		
		VFSItem img = tmpDir.resolve("IMG_1489.png");
		Assert.assertNull(img);
		Assert.assertFalse(success);
		
		targetDir.deleteSilently();
	}
}
	org.olat.core.util.ZipUtilTest.class,