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/PathUtils.java, src/main/java/org/olat/fileresource/types/FileResource.java, src/main/java/org/olat/fileresource/types/ImsCPFileResource.java, src/main/java/org/olat/fileresource/types/ImsQTI21Resource.java, src/main/java/org/olat/fileresource/types/ScormCPFileResource.java, src/main/java/org/olat/modules/wiki/WikiManager.java, src/test/java/org/olat/modules/ims/cp/ImsCPHandlerTest.java, src/test/java/org/olat/modules/wiki/WikiManagerTest.java, src/test/java/org/olat/test/AllTestsJunit4.java
Commit date: 2021-06-22 19:43:01 +0200
Commit message: OO-5549: fix the wiki import and add some unit tests
Diff: diff --git a/src/main/java/org/olat/core/util/PathUtils.java b/src/main/java/org/olat/core/util/PathUtils.java
index 6de5e5aeff..75a6b117fb 100644
--- a/src/main/java/org/olat/core/util/PathUtils.java
+++ b/src/main/java/org/olat/core/util/PathUtils.java
@@ -20,23 +20,26 @@
 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;
 import java.nio.file.Path;
 import java.nio.file.PathMatcher;
 import java.nio.file.Paths;
 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;
 
 /**
  * 
  * Initial date: 08.05.2014<br>
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
@@ -60,11 +63,12 @@ public class PathUtils {
 		Files.copy(source, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
 		return true;
 	}
 	
 	/**
-	 * 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
 	 * @param visitor The visitor
 	 * @return
@@ -88,18 +92,18 @@ public class PathUtils {
 			}
 		} else {
 			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);
 		}
 	}
 	
 	public static class YesMatcher implements PathMatcher {
 		@Override
@@ -123,10 +127,14 @@ public class PathUtils {
 		@Override
 		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 	    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);
 	        }
 	        return FileVisitResult.CONTINUE;
 		}
diff --git a/src/main/java/org/olat/fileresource/types/FileResource.java b/src/main/java/org/olat/fileresource/types/FileResource.java
index dcd6535fb4..bbd9aeb9de 100644
--- a/src/main/java/org/olat/fileresource/types/FileResource.java
+++ b/src/main/java/org/olat/fileresource/types/FileResource.java
@@ -28,24 +28,26 @@ package org.olat.fileresource.types;
 import java.io.File;
 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;
 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;
 import org.olat.core.util.PathUtils.CopyVisitor;
 import org.olat.core.util.PathUtils.YesMatcher;
@@ -191,11 +193,11 @@ 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;
 	}
 	
 	public static boolean copyResource(File file, String filename, File targetDirectory) {
 		return copyResource(file, filename, targetDirectory, new YesMatcher());
@@ -207,11 +209,11 @@ public class FileResource implements OLATResourceable {
 			if(path == null) {
 				return false;
 			}
 			
 			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) {
 			log.error("", e);
 			return false;
diff --git a/src/main/java/org/olat/fileresource/types/ImsCPFileResource.java b/src/main/java/org/olat/fileresource/types/ImsCPFileResource.java
index 5cdfc6d6ea..b05f51d106 100644
--- a/src/main/java/org/olat/fileresource/types/ImsCPFileResource.java
+++ b/src/main/java/org/olat/fileresource/types/ImsCPFileResource.java
@@ -25,15 +25,17 @@
 
 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;
 import java.util.Map;
 
@@ -73,11 +75,11 @@ public class ImsCPFileResource extends FileResource {
 			if(visitor.hasManifest()) {
 				Path realManifestPath = visitor.getManifestPath();
 				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 {
 					manifestPath = fPath.resolve(IMS_MANIFEST);
 				}
diff --git a/src/main/java/org/olat/fileresource/types/ImsQTI21Resource.java b/src/main/java/org/olat/fileresource/types/ImsQTI21Resource.java
index 4ad28dfe64..4ebc23c55a 100644
--- a/src/main/java/org/olat/fileresource/types/ImsQTI21Resource.java
+++ b/src/main/java/org/olat/fileresource/types/ImsQTI21Resource.java
@@ -27,15 +27,17 @@ package org.olat.fileresource.types;
 import java.io.File;
 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;
 import org.olat.core.util.PathUtils;
 import org.olat.ims.qti21.QTI21ContentPackage;
@@ -77,11 +79,11 @@ public class ImsQTI21Resource extends FileResource {
 			if(visitor.isValid()) {
 				Path realManifestPath = visitor.getManifestPath();
 				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 {
 					manifestPath = fPath.resolve(IMS_MANIFEST);
 				}
diff --git a/src/main/java/org/olat/fileresource/types/ScormCPFileResource.java b/src/main/java/org/olat/fileresource/types/ScormCPFileResource.java
index 0a05e43a3f..c346d095f5 100644
--- a/src/main/java/org/olat/fileresource/types/ScormCPFileResource.java
+++ b/src/main/java/org/olat/fileresource/types/ScormCPFileResource.java
@@ -25,15 +25,17 @@
 
 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;
 import java.util.List;
 import java.util.Map;
@@ -77,11 +79,11 @@ public class ScormCPFileResource extends FileResource {
 			if(visitor.isValid()) {
 				Path realManifestPath = visitor.getManifestPath();
 				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 {
 					manifestPath = fPath.resolve(IMS_MANIFEST);
 				}
diff --git a/src/main/java/org/olat/modules/wiki/WikiManager.java b/src/main/java/org/olat/modules/wiki/WikiManager.java
index 4473c5b248..6ceab36d59 100644
--- a/src/main/java/org/olat/modules/wiki/WikiManager.java
+++ b/src/main/java/org/olat/modules/wiki/WikiManager.java
@@ -28,17 +28,19 @@ package org.olat.modules.wiki;
 import java.io.File;
 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;
 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;
 
 import org.apache.commons.codec.binary.Base64;
@@ -160,11 +162,11 @@ public class WikiManager {
 			if(path == null) {
 				return false;
 			}
 			
 			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) {
 			log.error("", e);
 			return false;
@@ -297,30 +299,36 @@ public class WikiManager {
 		
 		@Override
 		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)
 		throws IOException {
 			final Path dirToCreate = Paths.get(destDir.toString(), dir.toString());
diff --git a/src/test/java/org/olat/modules/ims/cp/ImsCPHandlerTest.java b/src/test/java/org/olat/modules/ims/cp/ImsCPHandlerTest.java
new file mode 100644
index 0000000000..1a7b0dc0aa
--- /dev/null
+++ b/src/test/java/org/olat/modules/ims/cp/ImsCPHandlerTest.java
@@ -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());
+	}
+
+}
diff --git a/src/test/java/org/olat/modules/ims/cp/imscp.zip b/src/test/java/org/olat/modules/ims/cp/imscp.zip
new file mode 100644
index 0000000000..cb61d9c03e
Binary files /dev/null and b/src/test/java/org/olat/modules/ims/cp/imscp.zip differ
diff --git a/src/test/java/org/olat/modules/ims/cp/imscp_alt.zip b/src/test/java/org/olat/modules/ims/cp/imscp_alt.zip
new file mode 100644
index 0000000000..2130d88647
Binary files /dev/null and b/src/test/java/org/olat/modules/ims/cp/imscp_alt.zip differ
diff --git a/src/test/java/org/olat/modules/wiki/WikiManagerTest.java b/src/test/java/org/olat/modules/wiki/WikiManagerTest.java
new file mode 100644
index 0000000000..e26b29a6ae
--- /dev/null
+++ b/src/test/java/org/olat/modules/wiki/WikiManagerTest.java
@@ -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);
+	}
+
+}
diff --git a/src/test/java/org/olat/modules/wiki/wiki.zip b/src/test/java/org/olat/modules/wiki/wiki.zip
new file mode 100644
index 0000000000..5868472c8e
Binary files /dev/null and b/src/test/java/org/olat/modules/wiki/wiki.zip differ
diff --git a/src/test/java/org/olat/modules/wiki/wiki_alt.zip b/src/test/java/org/olat/modules/wiki/wiki_alt.zip
new file mode 100644
index 0000000000..8ba22243a3
Binary files /dev/null and b/src/test/java/org/olat/modules/wiki/wiki_alt.zip differ
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 383c7ef7e2..2698fbfbfc 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -234,10 +234,11 @@ import org.junit.runners.Suite;
 	org.olat.modules.dcompensation.manager.DisadvantageCompensationDAOTest.class,
 	org.olat.modules.dcompensation.manager.DisadvantageCompensationAuditLogDAOTest.class,
 	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,
 	org.olat.modules.coach.CoachingLargeTest.class,
 	org.olat.modules.curriculum.manager.CurriculumDAOTest.class,
@@ -357,10 +358,11 @@ import org.junit.runners.Suite;
 	org.olat.core.commons.services.notifications.manager.NotificationsManagerTest.class,
 	org.olat.registration.RegistrationManagerTest.class,
 	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,
 	org.olat.ims.qti.qpool.QTIExportProcessorTest.class,
 	org.olat.ims.qti.qpool.ItemFileResourceValidatorTest.class,

Vulnerable code: 	 * Use the closeSubsequentFS method to close the file system.
		    Files.walkFileTree(fPath, visitor);
			IOUtils.closeQuietly(path.getFileSystem());
import org.olat.core.id.OLATResourceable;
		Files.walkFileTree(fPath, rootSearcher);
			Files.walkFileTree(path, new CopyVisitor(path, destDir, filter));
				Files.walkFileTree(fPath, rootSearcher);
				Files.walkFileTree(fPath, rootSearcher);
				Files.walkFileTree(fPath, rootSearcher);
			Files.walkFileTree(path, new ImportVisitor(destDir));
			Path normalizedPath = file.normalize();
			if(!normalizedPath.startsWith(destDir)) {
				throw new IOException("Invalid ZIP");
			}
			
Fixed code: import java.nio.file.FileVisitOption;
import java.util.EnumSet;
import org.olat.core.logging.OLATRuntimeException;
	 * Use the closeSubsequentFS method to close the file system. The method doesn't
	 * follow sym. links and its depth is limited.
		    Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 32, visitor);
			IOUtils.closeQuietly(path.getFileSystem(), null);
	        Path normalizedPath = destFile.normalize();
			if(!normalizedPath.startsWith(destDir)) {
				throw new OLATRuntimeException("Invalid ZIP");
			}
import java.nio.file.FileVisitOption;
import java.util.EnumSet;
import org.olat.core.id.OLATResourceable;
		Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
			Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), 24, new CopyVisitor(path, destDir, filter));
import java.nio.file.FileVisitOption;
import java.util.EnumSet;
				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
import java.nio.file.FileVisitOption;
import java.util.EnumSet;
				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
import java.nio.file.FileVisitOption;
import java.util.EnumSet;
				Files.walkFileTree(fPath, EnumSet.noneOf(FileVisitOption.class), 16, rootSearcher);
import java.nio.file.FileVisitOption;
import java.util.EnumSet;
			Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), 16, new ImportVisitor(destDir));

	        	checkDestinationFile(destFile);
	        	checkDestinationFile(destFile);
				checkDestinationFile(destFile);
		
		private void checkDestinationFile(Path destFile) throws IOException {
			Path normalizedPath = destFile.normalize();
			if(!normalizedPath.startsWith(destDir)) {
				throw new OLATRuntimeException("Invalid ZIP");
			}
		}
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());
	}

}
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);
	}

}
	org.olat.modules.wiki.WikiManagerTest.class,
	org.olat.modules.ims.cp.ImsCPHandlerTest.class,