GitHub Security Advisory: GHSA-jjgj-cx3q-pw4w
Package: org.openmrs.web:openmrs-web (MAVEN)
Severity: HIGH (CVSS 7.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: >= 2.8.0, <= 2.8.5
Patched in: 2.8.6

Summary: OpenMRS ModuleResourcesServlet has Path Traversal that Leads to Arbitrary File Read

Details:
## Affected Versions

version ≤ 2.7.8 (latest version at time of disclosure)

https://github.com/openmrs/openmrs-core

## Impact

The `/openmrs/moduleResources/{moduleid}` endpoint in OpenMRS Core is vulnerable to a path traversal attack. The `ModuleResourcesServlet` does not properly validate user-supplied path input, allowing an attacker to traverse directories and read arbitrary files from the server filesystem (e.g., `/etc/passwd`, application configuration files containing database credentials).

This endpoint serves static module resources (CSS, JS, images) and is **not protected by authentication filters**, as these resources are required for rendering the login page. Therefore, this vulnerability can be exploited by an **unauthenticated** attacker.

> **Note:** Successful exploitation requires the target deployment to run on **Apache Tomcat < 8.5.31**, where the `..;` path parameter bypass is not mitigated by the container. Deployments on Tomcat ≥ 8.5.31 / ≥ 9.0.10 are protected at the container level, though the underlying code defect remains.
> 

## Steps to Reproduce

1. Identify a valid installed module ID on the target OpenMRS instance (e.g., `legacyui`).
2. Send the following HTTP request:

<img width="1038" height="798" alt="image" src="https://github.com/user-attachments/assets/7d10ee0e-4d81-4c01-bc84-a1bf5715f170" />

3. The server responds with HTTP 200 and the contents of `/etc/passwd`:

<img width="1028" height="843" alt="image" src="https://github.com/user-attachments/assets/b6806a7e-ff52-4f51-8f7f-7ea4e9754d10" />


## Root Cause Analysis

The vulnerability exists in `ModuleResourcesServlet.java` (`web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java`).

The `getFile()` method constructs a filesystem path from user-controlled input without performing path boundary validation:

```java
protected File getFile(HttpServletRequest request) {
    // Step 1: User-controlled path input
    String path = request.getPathInfo();

    // Step 2: Extract module from path prefix
    Module module = ModuleUtil.getModuleForPath(path);
    if (module == null) { return null; }

    // Step 3: Strip module ID prefix — no traversal check
    String relativePath = ModuleUtil.getPathForResource(module, path);

    // Step 4: Concatenate into absolute path
    String realPath = getServletContext().getRealPath("")
        + MODULE_PATH
        + module.getModuleIdAsPath()
        + "/resources"
        + relativePath;  // contains "/../../../etc/passwd"

    realPath = realPath.replace("/", File.separator);

    // Step 5: No normalize().startsWith() boundary check
    File f = new File(realPath);
    if (!f.exists()) { return null; }

    return f;  // Arbitrary file returned to client
}
```

The helper method `ModuleUtil.getPathForResource()` only strips the module ID prefix and performs no sanitization:

```java
public static String getPathForResource(Module module, String path) {
    if (path.startsWith("/")) {
        path = pa