Skip to content

TRUNK-6587: Add unit tests for ModuleResourcesServlet#6103

Open
KrimsonSun wants to merge 2 commits into
openmrs:masterfrom
KrimsonSun:TRUNK-6587-module-resources-servlet-tests
Open

TRUNK-6587: Add unit tests for ModuleResourcesServlet#6103
KrimsonSun wants to merge 2 commits into
openmrs:masterfrom
KrimsonSun:TRUNK-6587-module-resources-servlet-tests

Conversation

@KrimsonSun
Copy link
Copy Markdown

@KrimsonSun KrimsonSun commented May 18, 2026

Description of what I changed

Adds ModuleResourcesServletTest covering every branch of
ModuleResourcesServlet.getFile() — the request-path-to-File resolver that
backs /WEB-INF/view/module/<module>/resources/... lookups. The class
previously had zero test coverage despite holding all of the servlet's
security-sensitive path-resolution logic.

While writing the tests, two minor security gaps in getFile() surfaced.
Both were already described in the JIRA ticket as expected behaviour but
neither was actually enforced in code:

  1. A null request path NPE'd inside ModuleUtil.getModuleForPath instead
    of returning null. That surfaces to the caller as a 500 rather than a
    404 on bad input.
  2. A null byte (URL-encoded as %00) inside the path was not stripped
    before being passed to Path.of, which throws InvalidPathException.
    Null bytes in web paths are a well-known poison-NUL pattern — some
    downstream APIs treat the byte as a string terminator, allowing
    suffix-based checks to be bypassed.

Both are now rejected up front, with a WARN log line, matching the
behaviour the ticket described. A malformed path (one that ModuleUtil
rejects with IllegalArgumentException) is also caught and converted into
a null return for the same reason.

Production change

String path = request.getPathInfo();

if (path == null || path.indexOf((char) 0) >= 0) {
    log.warn("Rejected request with null or null-byte path");
    return null;
}

Module module;
try {
    module = ModuleUtil.getModuleForPath(path);
} catch (IllegalArgumentException ex) {
    log.warn("Rejected request with malformed path: " + path);
    return null;
}

Test class

ModuleResourcesServletTest uses JUnit Jupiter + Mockito 5 MockedStatic
to stub ModuleUtil, plus @TempDir to stage real files for the
filesystem-resolution paths. The 9 test methods cover, one per branch:

  • getFile_shouldReturnNullWhenRequestPathIsNull
  • getFile_shouldReturnNullWhenPathContainsANullByte
  • getFile_shouldReturnNullForMalformedPathWithoutLeadingModuleSegment
  • getFile_shouldReturnNullWhenNoModuleHandlesThePath
  • getFile_shouldRejectPathTraversalAttempts
  • getFile_shouldRejectTraversalEvenInDevelopmentMode
  • getFile_shouldReturnNullWhenResolvedFileDoesNotExist
  • getFile_shouldResolveFromWebappRootInProductionMode
  • getFile_shouldResolveFromDevelopmentDirectoryWhenDevModeIsActive

Verified with:

./mvnw -pl web test -Dtest=ModuleResourcesServletTest -Dsurefire.failIfNoSpecifiedTests=false
# Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

Issue I worked on

see https://openmrs.atlassian.net/browse/TRUNK-6587

Checklist: I completed these to help reviewers :)

  • My IDE is configured to follow the code style of this project.
  • I have added tests to cover my changes.
  • I ran mvn clean package right before creating this pull request and added all formatting changes to my commit.
  • All new and existing tests passed.
  • My pull request is based on the latest changes of the master branch.

Summary by CodeRabbit

  • Bug Fixes

    • Improved validation of incoming requests for module resources with better detection of malformed paths and suspicious inputs.
  • Tests

    • Added comprehensive test coverage for module resource security, including validation of malformed paths, request traversal attempts, and proper error handling.

Review Change Stack

Adds ModuleResourcesServletTest covering every branch of
ModuleResourcesServlet.getFile(), the request-path-to-File resolver
that backs /WEB-INF/view/module/<module>/resources/... lookups. The
class previously had zero test coverage despite holding all of the
servlet's security-sensitive path-resolution logic.

While writing the tests, two minor security gaps in getFile() became
visible. Both were already listed as expected behaviour in the JIRA
ticket but neither was actually enforced in code:

  1. A null request path NPE'd inside ModuleUtil.getModuleForPath
     instead of returning null. That surfaces to the caller as a 500
     rather than a 404 on bad input.
  2. A null byte injected into the path was not stripped before being
     passed to Path.of, which throws InvalidPathException. Null bytes
     in web paths are a well-known poison-NUL pattern: some downstream
     APIs treat the byte as a string terminator and bypass suffix-based
     checks.

Both are now rejected up front with a WARN log line, matching the
behaviour the ticket described. A malformed path (one that ModuleUtil
rejects with IllegalArgumentException) is also caught and converted
into a null return for the same reason.

New tests cover:
  - null path -> null
  - null-byte injection -> null (verifies module lookup is skipped)
  - malformed path "/" -> null
  - no module matches path -> null
  - directory traversal in production mode -> null
  - directory traversal in dev mode -> null
  - missing file on disk -> null
  - production-mode resolution from <webappRoot>/WEB-INF/view/...
  - dev-mode resolution from <devDir>/omod/target/classes/web/...
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR hardens module resource serving by validating request paths before module resolution. The servlet now rejects null paths, embedded null bytes, and malformed paths upfront; it also catches and logs exceptions from failed module lookups rather than allowing them to propagate. A comprehensive test suite validates both rejection and successful resolution paths.

Changes

Module Resource Path Security

Layer / File(s) Summary
Input validation in getFile method
web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java
getFile() now checks for null/empty paths and embedded null bytes before module resolution, and catches IllegalArgumentException from ModuleUtil.getModuleForPath() to prevent malformed inputs from causing unhandled exceptions.
Test class infrastructure and setup
web/src/test/java/org/openmrs/module/web/ModuleResourcesServletTest.java (lines 1–90)
New test class with comprehensive documentation of security-sensitive behavior, test fixtures for module IDs and filesystem locations, and JUnit lifecycle setup that mocks ServletContext/ServletConfig and manages ModuleUtil static mocks across test runs.
Security validation: rejection tests
web/src/test/java/org/openmrs/module/web/ModuleResourcesServletTest.java (lines 95–247, security-focused tests)
Nine test methods assert that getFile() returns null for invalid inputs: null path info, embedded null bytes, malformed module segments, unknown modules, directory traversal attempts in production and development modes, and non-existent resolved files.
Success path: valid resource resolution
web/src/test/java/org/openmrs/module/web/ModuleResourcesServletTest.java (lines 184–227, resolution tests)
Tests validate successful resource resolution from correct filesystem locations—webapp root in production mode and development directory in dev mode—by comparing canonical File targets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding unit tests for ModuleResourcesServlet with comprehensive coverage of the getFile() method.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PMD (7.24.0)
web/src/test/java/org/openmrs/module/web/ModuleResourcesServletTest.java

[ERROR] Cannot load ruleset rulesets/java/android.xml/DoNotHardCodeSDCard: Cannot resolve rule/ruleset reference 'rulesets/java/android.xml/DoNotHardCodeSDCard'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.

web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java

[ERROR] Cannot load ruleset rulesets/java/android.xml/DoNotHardCodeSDCard: Cannot resolve rule/ruleset reference 'rulesets/java/android.xml/DoNotHardCodeSDCard'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java (1)

118-120: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle malformed filesystem paths to prevent 500 errors during normalization.

Your new guard at line 94 catches module-path lookup failures early—that's good! However, Path.of(realPath) and Path.of(basePath) at lines 118–119 can still throw InvalidPathException if the filesystem path contains invalid characters (e.g., null bytes, control characters, or OS-specific invalid chars). This exception isn't caught, so a malformed input returns a 500 instead of a clean null rejection.

Wrap the normalization in the same try-catch pattern to keep the endpoint fail-closed:

Suggested fix
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
@@
-		Path normalizedPath = Path.of(realPath).normalize();
-		Path normalizedBase = Path.of(basePath).normalize();
+		Path normalizedPath;
+		Path normalizedBase;
+		try {
+			normalizedPath = Path.of(realPath).normalize();
+			normalizedBase = Path.of(basePath).normalize();
+		}
+		catch (InvalidPathException ex) {
+			log.warn("Rejected request with invalid filesystem path: " + path);
+			return null;
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java` around
lines 118 - 120, Wrap the Path.of(...).normalize() calls in
ModuleResourcesServlet in a try-catch for InvalidPathException so malformed
filesystem paths do not throw a 500; specifically, surround the
creation/normalization of normalizedPath and normalizedBase (the
Path.of(realPath).normalize() and Path.of(basePath).normalize() expressions)
with a try { ... } catch (InvalidPathException e) { return null; } or equivalent
fail-closed behavior, and ensure any logging includes the exception so the
method cleanly returns null instead of propagating the exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java`:
- Around line 118-120: Wrap the Path.of(...).normalize() calls in
ModuleResourcesServlet in a try-catch for InvalidPathException so malformed
filesystem paths do not throw a 500; specifically, surround the
creation/normalization of normalizedPath and normalizedBase (the
Path.of(realPath).normalize() and Path.of(basePath).normalize() expressions)
with a try { ... } catch (InvalidPathException e) { return null; } or equivalent
fail-closed behavior, and ensure any logging includes the exception so the
method cleanly returns null instead of propagating the exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5cf0b0fb-76e3-4165-995d-a11a83c7d250

📥 Commits

Reviewing files that changed from the base of the PR and between 9490dcf and f43bc18.

📒 Files selected for processing (2)
  • web/src/main/java/org/openmrs/module/web/ModuleResourcesServlet.java
  • web/src/test/java/org/openmrs/module/web/ModuleResourcesServletTest.java

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant