CVE-2021-39180 (published 2021-08-31T18:15Z)

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.

Severity: HIGH
CVSS v2 base score: 9.0 (AV:N/AC:L/Au:S/C:C/I:C/A:C)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/OpenOLAT/OpenOLAT
commit_details: repo_url: https://github.com/OpenOLAT/OpenOLAT
author: srosse
author_date: 2021-07-02 09:04:25+02:00
msg: OO-5549: replace the wildfly assert by a junit one
repository: repo_url: https://github.com/OpenOLAT/OpenOLAT
repo_name: OpenOLAT/OpenOLAT
description: Learning Management System OpenOlat
repo_language: Java
owner: OpenOLAT
file_changes: filename: WikiManagerTest.java
change_type: ModificationType.MODIFY
diff: @@ -24,6 +24,7 @@ import java.net.URISyntaxException;
 import java.net.URL;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.olat.core.logging.OLATRuntimeException;
@@ -32,7 +33,6 @@ import org.olat.core.util.FileUtils;
 import org.olat.core.util.WebappHelper;
 import org.olat.test.OlatTestCase;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.wildfly.common.Assert;
 
 /**
  * 

programming_language: Java
repo_url: https://github.com/OpenOLAT/OpenOLAT
commit_details: repo_url: https://github.com/OpenOLAT/OpenOLAT
author: srosse
author_date: 2021-06-22 19:43:01+02:00
msg: OO-5549: fix the wiki import and add some unit tests
repository: repo_url: https://github.com/OpenOLAT/OpenOLAT
repo_name: OpenOLAT/OpenOLAT
description: Learning Management System OpenOlat
repo_language: Java
owner: OpenOLAT
file_changes: filename: PathUtils.java
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ package org.olat.core.util;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.FileSystems;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.FileVisitor;
 import java.nio.file.Files;
@@ -32,9 +33,11 @@ import java.nio.file.ProviderNotFoundException;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
 import java.util.ServiceConfigurationError;
 
 import org.apache.commons.io.IOUtils;
+import org.olat.core.logging.OLATRuntimeException;
 
 /**
  * 
@@ -62,7 +65,8 @@ public class PathUtils {
 	}
 	
 	/**
-	 * Use the closeSubsequentFS method to close the file system.
+	 * Use the closeSubsequentFS method to close the file system. The method doesn't
+	 * follow sym. links and its depth is limited.
 	 * 
 	 * @param file The file to visit
 	 * @param filename The filename
@@ -90,14 +94,14 @@ public class PathUtils {
 			fPath = file.toPath();
 		}
 		if(fPath != null) {
-		    Files.walkFileTree(fPath, visitor);
+		    Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 32, visitor);
 		}
 		return fPath;
 	}
 	
 	public static void closeSubsequentFS(Path path) {
 		if(path != null && FileSystems.getDefault() != path.getFileSystem()) {
-			IOUtils.closeQuietly(path.getFileSystem());
+			IOUtils.closeQuietly(path.getFileSystem(), null);
 		}
 	}
 	
@@ -125,6 +129,10 @@ public class PathUtils {
 	    throws IOException {
 			Path relativeFile = source.relativize(file);
 	        final Path destFile = Paths.get(destDir.toString(), relativeFile.toString());
+	        Path normalizedPath = destFile.normalize();
+			if(!normalizedPath.startsWith(destDir)) {
+				throw new OLATRuntimeException("Invalid ZIP");
+			}
 	        if(filter.matches(file)) {
 	        	Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
 	        }

programming_language: Java
filename: FileResource.java
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,7 @@ import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -37,13 +38,14 @@ import java.nio.file.PathMatcher;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.spi.FileSystemProvider;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
-import org.olat.core.id.OLATResourceable;
 import org.apache.logging.log4j.Logger;
+import org.olat.core.id.OLATResourceable;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.PathUtils;
@@ -193,7 +195,7 @@ public class FileResource implements OLATResourceable {
 	protected static  RootSearcher searchRootDirectory(Path fPath)
 	throws IOException {
 		RootSearcher rootSearcher = new RootSearcher();
-		Files.walkFileTree(fPath, rootSearcher);
+		Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
 		return rootSearcher;
 	}
 	
@@ -209,7 +211,7 @@ public class FileResource implements OLATResourceable {
 			}
 			
 			Path destDir = targetDirectory.toPath();
-			Files.walkFileTree(path, new CopyVisitor(path, destDir, filter));
+			Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), 24, new CopyVisitor(path, destDir, filter));
 			PathUtils.closeSubsequentFS(path);
 			return true;
 		} catch (IOException e) {

programming_language: Java
filename: ImsCPFileResource.java
change_type: ModificationType.MODIFY
diff: @@ -27,11 +27,13 @@ package org.olat.fileresource.types;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -75,7 +77,7 @@ public class ImsCPFileResource extends FileResource {
 				Path manifestPath = fPath.resolve(realManifestPath);
 				
 				RootSearcher rootSearcher = new RootSearcher();
-				Files.walkFileTree(fPath, rootSearcher);
+				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
 				if(rootSearcher.foundRoot()) {
 					manifestPath = rootSearcher.getRoot().resolve(IMS_MANIFEST);
 				} else {

programming_language: Java
filename: ImsQTI21Resource.java
change_type: ModificationType.MODIFY
diff: @@ -29,11 +29,13 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
 
 import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
@@ -79,7 +81,7 @@ public class ImsQTI21Resource extends FileResource {
 				Path manifestPath = fPath.resolve(realManifestPath);
 				
 				RootSearcher rootSearcher = new RootSearcher();
-				Files.walkFileTree(fPath, rootSearcher);
+				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
 				if(rootSearcher.foundRoot()) {
 					manifestPath = rootSearcher.getRoot().resolve(IMS_MANIFEST);
 				} else {

programming_language: Java
filename: ScormCPFileResource.java
change_type: ModificationType.MODIFY
diff: @@ -27,11 +27,13 @@ package org.olat.fileresource.types;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -79,7 +81,7 @@ public class ScormCPFileResource extends FileResource {
 				Path manifestPath = fPath.resolve(realManifestPath);
 				
 				RootSearcher rootSearcher = new RootSearcher();
-				Files.walkFileTree(fPath, rootSearcher);
+				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
 				if(rootSearcher.foundRoot()) {
 					manifestPath = rootSearcher.getRoot().resolve(IMS_MANIFEST);
 				} else {

programming_language: Java
filename: WikiManager.java
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -37,6 +38,7 @@ import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
@@ -162,7 +164,7 @@ public class WikiManager {
 			}
 			
 			Path destDir = targetDirectory.toPath();
-			Files.walkFileTree(path, new ImportVisitor(destDir));
+			Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), 16, new ImportVisitor(destDir));
 			PathUtils.closeSubsequentFS(path);
 			return true;
 		} catch (IOException e) {
@@ -299,26 +301,32 @@ public class WikiManager {
 		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 	    throws IOException {
 			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);
+	        	checkDestinationFile(destFile);
 	        	resetAndCopyProperties(file, destFile);
 	        } else if (filename.endsWith(WIKI_FILE_SUFFIX)) {
 	        	String f = convertAlternativeFilename(file.toString());
 	        	final Path destFile = Paths.get(wikiDir.toString(), f);
+	        	checkDestinationFile(destFile);
 	        	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());
+				checkDestinationFile(destFile);
 				Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
 			}
 	        return FileVisitResult.CONTINUE;
 		}
+		
+		private void checkDestinationFile(Path destFile) throws IOException {
+			Path normalizedPath = destFile.normalize();
+			if(!normalizedPath.startsWith(destDir)) {
+				throw new OLATRuntimeException("Invalid ZIP");
+			}
+		}
 	 
 		@Override
 		public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)

programming_language: Java
filename: ImsCPHandlerTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,61 @@
+package org.olat.modules.ims.cp;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.id.Identity;
+import org.olat.core.logging.OLATRuntimeException;
+import org.olat.fileresource.FileResourceManager;
+import org.olat.fileresource.types.ImsCPFileResource;
+import org.olat.repository.RepositoryEntry;
+import org.olat.repository.handlers.RepositoryHandler;
+import org.olat.repository.handlers.RepositoryHandlerFactory;
+import org.olat.test.JunitTestHelper;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 22 juin 2021<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ImsCPHandlerTest extends OlatTestCase {
+	
+	@Autowired
+	private RepositoryHandlerFactory handlerFactory;
+	
+	@Test
+	public void importImsCP() throws URISyntaxException {
+		Identity author = JunitTestHelper.createAndPersistIdentityAsRndAuthor("ims-cp-1");
+		URL imsCpUrl = ImsCPHandlerTest.class.getResource("imscp.zip");
+		File imsCpFile = new File(imsCpUrl.toURI());
+		
+		RepositoryHandler cpHandler = handlerFactory.getRepositoryHandler(ImsCPFileResource.TYPE_NAME);	
+		RepositoryEntry entry = cpHandler.importResource(author, null, "IMS CP", null, false, null, Locale.ENGLISH, imsCpFile, imsCpFile.getName());
+		Assert.assertNotNull(entry);
+		
+		File cpRoot = FileResourceManager.getInstance().unzipFileResource(entry.getOlatResource());
+		File image = new File(cpRoot, "IMG_1482.jpg");
+		Assert.assertTrue(image.exists());
+		File manifestXml = new File(cpRoot, "imsmanifest.xml");
+		Assert.assertTrue(manifestXml.exists());
+		File page = new File(cpRoot, "new.html");
+		Assert.assertTrue(page.exists());
+	}
+	
+	@Test(expected=OLATRuntimeException.class)
+	public void importImsCPSlide() throws URISyntaxException {
+		Identity author = JunitTestHelper.createAndPersistIdentityAsRndAuthor("ims-cp-1");
+		URL imsCpUrl = ImsCPHandlerTest.class.getResource("imscp_alt.zip");
+		File imsCpFile = new File(imsCpUrl.toURI());
+		
+		RepositoryHandler cpHandler = handlerFactory.getRepositoryHandler(ImsCPFileResource.TYPE_NAME);	
+		cpHandler.importResource(author, null, "IMS CP", null, false, null, Locale.ENGLISH, imsCpFile, imsCpFile.getName());
+	}
+
+}

programming_language: Java
filename: WikiManagerTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,62 @@
+package org.olat.modules.wiki;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.olat.core.logging.OLATRuntimeException;
+import org.olat.core.util.CodeHelper;
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.WebappHelper;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.wildfly.common.Assert;
+
+/**
+ * 
+ * Initial date: 22 juin 2021<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class WikiManagerTest extends OlatTestCase {
+	
+	@Autowired
+	private WikiManager wikiManager;
+	
+	private File tmpWikiDir;
+	
+	@Before
+	public void createTmpDir() {
+		tmpWikiDir = new File(WebappHelper.getTmpDir(), "wiki" + CodeHelper.getForeverUniqueID());
+	}
+	
+	@After
+	public void deleteTmpDir() {
+		FileUtils.deleteDirsAndFiles(tmpWikiDir, true, true);
+	}
+	
+	@Test
+	public void importWiki() throws URISyntaxException {
+		URL wikiUrl = WikiManagerTest.class.getResource("wiki.zip");
+		File wikiFile = new File(wikiUrl.toURI());
+		wikiManager.importWiki(wikiFile, null, tmpWikiDir);
+		
+		File image = new File(tmpWikiDir, "media/IMG_1482.jpg");
+		Assert.assertTrue(image.exists());
+		File imageMetadata = new File(tmpWikiDir, "media/IMG_1482.jpg.metadata");
+		Assert.assertTrue(imageMetadata.exists());
+		File indexPage = new File(tmpWikiDir, "wiki/SW5kZXg=.wp");
+		Assert.assertTrue(indexPage.exists());
+	}
+	
+	@Test(expected=OLATRuntimeException.class)
+	public void importWikiSlide() throws URISyntaxException {
+		URL wikiUrl = WikiManagerTest.class.getResource("wiki_alt.zip");
+		File wikiFile = new File(wikiUrl.toURI());
+		wikiManager.importWiki(wikiFile, null, tmpWikiDir);
+	}
+
+}

programming_language: Java
filename: AllTestsJunit4.java
change_type: ModificationType.MODIFY
diff: @@ -236,6 +236,7 @@ import org.junit.runners.Suite;
 	org.olat.modules.iq.IQManagerTest.class,
 	org.olat.modules.fo.ForumManagerTest.class,//fail
 	org.olat.modules.wiki.WikiUnitTest.class,
+	org.olat.modules.wiki.WikiManagerTest.class,
 	org.olat.modules.wiki.versioning.diff.CookbookDiffTest.class,
 	org.olat.modules.wiki.gui.components.wikiToHtml.FilterUtilTest.class,
 	org.olat.modules.coach.manager.CoachingDAOTest.class,
@@ -359,6 +360,7 @@ import org.junit.runners.Suite;
 	org.olat.course.nodes.projectbroker.ProjectBrokerManagerTest.class,
 	org.olat.core.commons.persistence.DBTest.class,
 	org.olat.modules.ims.cp.CPManagerTest.class,
+	org.olat.modules.ims.cp.ImsCPHandlerTest.class,
 	org.olat.modules.ims.qti.fileresource.FileResourceValidatorTest.class,
 	org.olat.ims.qti.QTIResultManagerTest.class,
 	org.olat.ims.qti.qpool.QTIImportProcessorTest.class,

programming_language: Java
repo_url: https://github.com/OpenOLAT/OpenOLAT
commit_details: repo_url: https://github.com/OpenOLAT/OpenOLAT
author: srosse
author_date: 2021-06-21 16:32:21+02:00
msg: OO-5549: check parent by unzip
repository: repo_url: https://github.com/OpenOLAT/OpenOLAT
repo_name: OpenOLAT/OpenOLAT
description: Learning Management System OpenOlat
repo_language: Java
owner: OpenOLAT
file_changes: filename: ZipUtil.java
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,7 @@ 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;
@@ -188,20 +189,24 @@ public class ZipUtil {
 	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('/');
@@ -317,7 +322,7 @@ public class ZipUtil {
 			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) {
@@ -455,7 +460,7 @@ public class ZipUtil {
 						// skip MacOSX specific metadata directory
 						// directories aren't locked
 						oZip.closeEntry();
-						oEntr = oZip.getNextEntry();
+						oEntr = oZip.getNextEntry();//TODO zip
 						continue;
 					} else {
 						// search file
@@ -847,9 +852,17 @@ public class ZipUtil {
 	 * @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();

programming_language: Java
filename: LocalFolderImpl.java
change_type: ModificationType.MODIFY
diff: @@ -335,6 +335,14 @@ public class LocalFolderImpl extends LocalImpl implements VFSContainer {
 		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();

programming_language: Java
filename: MergeSource.java
change_type: ModificationType.MODIFY
diff: @@ -202,6 +202,11 @@ public class MergeSource extends AbstractVirtualContainer {
 		return rootWriteContainer.createChildLeaf(name);
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return rootWriteContainer.isInPath(path);
+	}
+
 	@Override
 	public VFSStatus copyFrom(VFSItem source) {
 		if (canWrite() != VFSConstants.YES) {

programming_language: Java
filename: NamedContainerImpl.java
change_type: ModificationType.MODIFY
diff: @@ -121,6 +121,11 @@ public class NamedContainerImpl extends AbstractVirtualContainer {
 		return getDelegate().getRelPath();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return getDelegate().isInPath(path);
+	}
+
 	@Override
 	public VFSStatus delete() {
 		return getDelegate().delete();

programming_language: Java
filename: VFSContainer.java
change_type: ModificationType.MODIFY
diff: @@ -86,6 +86,13 @@ public interface VFSContainer extends VFSItem {
 	 */
 	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
 	 * 

programming_language: Java
filename: VirtualContainer.java
change_type: ModificationType.MODIFY
diff: @@ -105,6 +105,11 @@ public class VirtualContainer extends AbstractVirtualContainer {
 		return null;
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
+
 	@Override
 	public VFSItem resolve(String path) {
 		if(path != null && path.length() > 1 && path.startsWith("/")) {

programming_language: Java
filename: DataStepForm.java
change_type: ModificationType.MODIFY
diff: @@ -366,7 +366,7 @@ public class DataStepForm extends StepFormBasicController {
 			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

programming_language: Java
filename: VFSCPContainer.java
change_type: ModificationType.MODIFY
diff: @@ -96,6 +96,11 @@ public class VFSCPContainer extends AbstractVirtualContainer implements VFSConta
 	public String getRelPath() {
 		return null;
 	}
+	
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
 
 	@Override
 	public List<VFSItem> getItems() {

programming_language: Java
filename: VFSCPNamedContainerItem.java
change_type: ModificationType.MODIFY
diff: @@ -71,6 +71,11 @@ public class VFSCPNamedContainerItem extends NamedLeaf implements VFSContainer {
 		return getItems();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return false;
+	}
+
 	@Override
 	public VFSStatus copyFrom(VFSItem source) {
 		return VFSConstants.NO;

programming_language: Java
filename: VFSMediaFilesContainer.java
change_type: ModificationType.MODIFY
diff: @@ -85,6 +85,11 @@ public class VFSMediaFilesContainer extends AbstractVirtualContainer implements
 		return rootContainer.getRelPath();
 	}
 
+	@Override
+	public boolean isInPath(String path) {
+		return rootContainer.isInPath(path);
+	}
+
 	@Override
 	public VFSContainer getParentContainer() {
 		return null;

programming_language: Java
filename: VFSRootCPContainer.java
change_type: ModificationType.MODIFY
diff: @@ -112,6 +112,11 @@ public class VFSRootCPContainer extends AbstractVirtualContainer implements VFSC
 		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)

programming_language: Java
filename: QTI21ImportProcessor.java
change_type: ModificationType.MODIFY
diff: @@ -161,6 +161,11 @@ public class QTI21ImportProcessor {
 			if(Files.notExists(assessmentItemPath)) {
 				return null;
 			}
+			
+			Path normalizedPath = assessmentItemPath.normalize();
+			if(!normalizedPath.startsWith(parentPath)) {
+				throw new IOException("Invalid Item");
+			}
 	
 			String dir = qpoolFileStorage.generateDir();
 			//storage
@@ -213,6 +218,10 @@ public class QTI21ImportProcessor {
 			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);
 				}
 			}

programming_language: Java
filename: CopyAndConvertVisitor.java
change_type: ModificationType.MODIFY
diff: @@ -115,8 +115,10 @@ class CopyAndConvertVisitor extends SimpleFileVisitor<Path> {
 			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);
 			}
 		}
@@ -127,13 +129,22 @@ class CopyAndConvertVisitor extends SimpleFileVisitor<Path> {
 	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
 	 * 

programming_language: Java
filename: WikiManager.java
change_type: ModificationType.MODIFY
diff: @@ -298,15 +298,20 @@ 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());

programming_language: Java
filename: ZipUtilTest.java
change_type: ModificationType.ADD
diff: @@ -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();
+	}
+}

programming_language: Java
filename: AllTestsJunit4.java
change_type: ModificationType.MODIFY
diff: @@ -73,6 +73,7 @@ import org.junit.runners.Suite;
 	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,

programming_language: Java