{"id":"GHSA-jjgj-cx3q-pw4w","summary":"OpenMRS ModuleResourcesServlet has Path Traversal that Leads to Arbitrary File Read","details":"## Affected Versions\n\nversion ≤ 2.7.8 (latest version at time of disclosure)\n\nhttps://github.com/openmrs/openmrs-core\n\n## Impact\n\nThe `/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).\n\nThis 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.\n\n\u003e **Note:** Successful exploitation requires the target deployment to run on **Apache Tomcat \u003c 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.\n\u003e \n\n## Steps to Reproduce\n\n1. Identify a valid installed module ID on the target OpenMRS instance (e.g., `legacyui`).\n2. Send the following HTTP request:\n\n\u003cimg width=\"1038\" height=\"798\" alt=\"image\" src=\"https://github.com/user-attachments/assets/7d10ee0e-4d81-4c01-bc84-a1bf5715f170\" /\u003e\n\n3. The server responds with HTTP 200 and the contents of `/etc/passwd`:\n\n\u003cimg width=\"1028\" height=\"843\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b6806a7e-ff52-4f51-8f7f-7ea4e9754d10\" /\u003e\n\n\n## Root Cause Analysis\n\nThe vulnerability exists in `ModuleResourcesServlet.java` (`web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java`).\n\nThe `getFile()` method constructs a filesystem path from user-controlled input without performing path boundary validation:\n\n```java\nprotected File getFile(HttpServletRequest request) {\n    // Step 1: User-controlled path input\n    String path = request.getPathInfo();\n\n    // Step 2: Extract module from path prefix\n    Module module = ModuleUtil.getModuleForPath(path);\n    if (module == null) { return null; }\n\n    // Step 3: Strip module ID prefix — no traversal check\n    String relativePath = ModuleUtil.getPathForResource(module, path);\n\n    // Step 4: Concatenate into absolute path\n    String realPath = getServletContext().getRealPath(\"\")\n        + MODULE_PATH\n        + module.getModuleIdAsPath()\n        + \"/resources\"\n        + relativePath;  // contains \"/../../../etc/passwd\"\n\n    realPath = realPath.replace(\"/\", File.separator);\n\n    // Step 5: No normalize().startsWith() boundary check\n    File f = new File(realPath);\n    if (!f.exists()) { return null; }\n\n    return f;  // Arbitrary file returned to client\n}\n```\n\nThe helper method `ModuleUtil.getPathForResource()` only strips the module ID prefix and performs no sanitization:\n\n```java\npublic static String getPathForResource(Module module, String path) {\n    if (path.startsWith(\"/\")) {\n        path = path.substring(1);\n    }\n    return path.substring(module.getModuleIdAsPath().length());\n    // Returns unsanitized remainder, e.g., \"/../../../../../../etc/passwd\"\n}\n```\n\nThe resulting path resolves as:\n\n```\n{webapp}/WEB-INF/view/module/legacyui/resources/../../../../../../etc/passwd\n  → /etc/passwd\n```\n\nNotably, the same codebase already implements correct path traversal protection in `StartupFilter.java`:\n\n```java\n// StartupFilter.java — correct protection\nfullFilePath = fullFilePath.resolve(httpRequest.getPathInfo());\nif (!(fullFilePath.normalize().startsWith(filePath))) {\n    log.warn(\"Detected attempted directory traversal...\");\n    return;  // Request rejected\n}\n```\n\nThis check is absent from `ModuleResourcesServlet`.\n\n## Remediation\n\nAdd a path boundary check after constructing `realPath` and before returning the `File` object. The fix should use `normalize()` + `startsWith()` to ensure the resolved path stays within the allowed module resources directory:\n\n```java\nFile f = new File(realPath);\nPath allowedBase = Paths.get(getServletContext().getRealPath(\"\"), \"WEB-INF\", \"view\", \"module\");\nif (!f.toPath().normalize().startsWith(allowedBase.normalize())) {\n    log.warn(\"Blocked path traversal attempt: {}\", request.getPathInfo());\n    return null;\n}\n```\n\nThis is consistent with the existing pattern used in `StartupFilter.java` and `TestInstallUtil.java` within the same project.","aliases":["CVE-2026-40075"],"modified":"2026-05-08T15:51:33.312260Z","published":"2026-05-04T17:18:48Z","database_specific":{"github_reviewed_at":"2026-05-04T17:18:48Z","github_reviewed":true,"cwe_ids":["CWE-22"],"nvd_published_at":"2026-05-05T22:16:00Z","severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/openmrs/openmrs-core/security/advisories/GHSA-jjgj-cx3q-pw4w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40075"},{"type":"PACKAGE","url":"https://github.com/openmrs/openmrs-core"}],"affected":[{"package":{"name":"org.openmrs.web:openmrs-web","ecosystem":"Maven","purl":"pkg:maven/org.openmrs.web/openmrs-web"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.7.8"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-jjgj-cx3q-pw4w/GHSA-jjgj-cx3q-pw4w.json"}},{"package":{"name":"org.openmrs.web:openmrs-web","ecosystem":"Maven","purl":"pkg:maven/org.openmrs.web/openmrs-web"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.8.0"},{"fixed":"2.8.6"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 2.8.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-jjgj-cx3q-pw4w/GHSA-jjgj-cx3q-pw4w.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"}]}