CVE-2022-23612: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: OpenMRS is a patient-based medical record system focusing on giving providers a free customizable electronic medical record system. Affected versions are subject to arbitrary file exfiltration due to failure to sanitize request when satisfying GET requests for `/images` & `/initfilter/scripts`. This can allow an attacker to access any file on a system running OpenMRS that is accessible to the user id OpenMRS is running under. Affected implementations should update to the latest patch version of OpenMRS Core for the minor version they use. These are: 2.1.5, 2.2.1, 2.3.5, 2.4.5 and 2.5.3. As a general rule, this vulnerability is already mitigated by Tomcat's URL normalization in Tomcat 7.0.28+. Users on older versions of Tomcat should consider upgrading their Tomcat instance as well as their OpenMRS instance.
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: 7.5
Published: 2022-02-22T23:15Z
Repository: https://github.com/openmrs/openmrs-core
Language: Java
Files changed: web/src/main/java/org/openmrs/web/filter/StartupFilter.java
Commit date: 2022-01-28 15:58:59 -0500
Commit message: Fix bug

Diff: diff --git a/web/src/main/java/org/openmrs/web/filter/StartupFilter.java b/web/src/main/java/org/openmrs/web/filter/StartupFilter.java
index b86177e1a..883291857 100644
--- a/web/src/main/java/org/openmrs/web/filter/StartupFilter.java
+++ b/web/src/main/java/org/openmrs/web/filter/StartupFilter.java
@@ -16,10 +16,12 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -114,34 +116,31 @@ public abstract class StartupFilter implements Filter {
 			String servletPath = httpRequest.getServletPath();
 			// for all /images and /initfilter/scripts files, write the path
 			// (the "/initfilter" part is needed so that the openmrs_static_context-servlet.xml file doesn't
 			//  get instantiated early, before the locale messages are all set up)
 			if (servletPath.startsWith("/images") || servletPath.startsWith("/initfilter/scripts")) {
-				servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view"); // strip out the /initfilter part
+				// strip out the /initfilter part
+				servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view");
 				// writes the actual image file path to the response
-				File file = new File(filterConfig.getServletContext().getRealPath(servletPath));
+				Path filePath = Paths.get(filterConfig.getServletContext().getRealPath(servletPath)).normalize();
+				Path fullFilePath = filePath;
 				if (httpRequest.getPathInfo() != null) {
-					file = new File(file, httpRequest.getPathInfo());
+					fullFilePath = fullFilePath.resolve(httpRequest.getPathInfo());
+					if (!(fullFilePath.normalize().startsWith(filePath))) {
+						log.warn("Detected attempted directory traversal in request for {}", httpRequest.getPathInfo());
+						return;
+					}
 				}
 				
-				InputStream imageFileInputStream = null;
-				try {
-					imageFileInputStream = new FileInputStream(file);
+				try (InputStream imageFileInputStream = new FileInputStream(fullFilePath.normalize().toFile())) {
 					OpenmrsUtil.copyFile(imageFileInputStream, httpResponse.getOutputStream());
 				}
 				catch (FileNotFoundException e) {
-					log.error("Unable to find file: " + file.getAbsolutePath());
+					log.error("Unable to find file: {}", filePath, e);
 				}
-				finally {
-					if (imageFileInputStream != null) {
-						try {
-							imageFileInputStream.close();
-						}
-						catch (IOException io) {
-							log.warn("Couldn't close imageFileInputStream: " + io);
-						}
-					}
+				catch (IOException e) {
+					log.warn("An error occurred while handling file {}", filePath, e);
 				}
 			} else if (servletPath.startsWith("/scripts")) {
 				log.error(
 				    "Calling /scripts during the initializationfilter pages will cause the openmrs_static_context-servlet.xml to initialize too early and cause errors after startup.  Use '/initfilter"
 				            + servletPath + "' instead.");
@@ -191,11 +190,11 @@ public abstract class StartupFilter implements Filter {
 			
 			try {
 				velocityEngine.init(props);
 			}
 			catch (Exception e) {
-				log.error("velocity init failed, because: " + e);
+				log.error("velocity init failed, because: {}", e, e);
 			}
 		}
 	}
 	
 	/**

Vulnerable code: 				servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view"); // strip out the /initfilter part
				File file = new File(filterConfig.getServletContext().getRealPath(servletPath));
					file = new File(file, httpRequest.getPathInfo());
				InputStream imageFileInputStream = null;
				try {
					imageFileInputStream = new FileInputStream(file);
					log.error("Unable to find file: " + file.getAbsolutePath());
				finally {
					if (imageFileInputStream != null) {
						try {
							imageFileInputStream.close();
						}
						catch (IOException io) {
							log.warn("Couldn't close imageFileInputStream: " + io);
						}
					}
				log.error("velocity init failed, because: " + e);
Fixed code: import java.nio.file.Path;
import java.nio.file.Paths;
				// strip out the /initfilter part
				servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view");
				Path filePath = Paths.get(filterConfig.getServletContext().getRealPath(servletPath)).normalize();
				Path fullFilePath = filePath;
					fullFilePath = fullFilePath.resolve(httpRequest.getPathInfo());
					if (!(fullFilePath.normalize().startsWith(filePath))) {
						log.warn("Detected attempted directory traversal in request for {}", httpRequest.getPathInfo());
						return;
					}
				try (InputStream imageFileInputStream = new FileInputStream(fullFilePath.normalize().toFile())) {
					log.error("Unable to find file: {}", filePath, e);
				catch (IOException e) {
					log.warn("An error occurred while handling file {}", filePath, e);
				log.error("velocity init failed, because: {}", e, e);