CVE-2021-39180 (Java, published 2021-08-31T18:15Z)
Description: 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.
Severity: HIGH (CVSS v3 base score: 8.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/OpenOLAT/OpenOLAT
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 message: OO-5549: check parent by unzip

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,