From cb06005bd3d03426f26dc703c07106645058907c Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Mon, 3 Aug 2026 19:11:31 +0530 Subject: [PATCH] validate path, query and fragment for authority-less file URLs --- .../validator/routines/UrlValidator.java | 9 +++-- .../validator/routines/UrlValidatorTest.java | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/commons/validator/routines/UrlValidator.java b/src/main/java/org/apache/commons/validator/routines/UrlValidator.java index 013da73e3..4d100dce0 100644 --- a/src/main/java/org/apache/commons/validator/routines/UrlValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/UrlValidator.java @@ -384,11 +384,12 @@ public boolean isValid(final String value) { return false; } final String authority = uri.getRawAuthority(); - if ("file".equals(scheme) && GenericValidator.isBlankOrNull(authority)) { // Special case - file: allows an empty authority - return true; // this is a local file - nothing more to do here - } + // Special case - file: allows an empty authority, so only the authority check is skipped for it; + // the path, query and fragment below are validated as they are for any other scheme + final boolean emptyFileAuthority = "file".equals(scheme) && GenericValidator.isBlankOrNull(authority); // Validate the authority - if ("file".equals(scheme) && authority != null && authority.contains(":") || !isValidAuthority(authority)) { + if (!emptyFileAuthority + && ("file".equals(scheme) && authority != null && authority.contains(":") || !isValidAuthority(authority))) { return false; } if (!isValidPath(uri.getRawPath()) || !isValidQuery(uri.getRawQuery()) || !isValidFragment(uri.getRawFragment())) { diff --git a/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java index 4de098a80..c18fe7d99 100644 --- a/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java @@ -152,6 +152,45 @@ public void setUp() { } } + @Test + void testFileSchemePath() { + final String[] schemes = { "file" }; + final UrlValidator urlValidator = new UrlValidator(schemes, UrlValidator.ALLOW_LOCAL_URLS); + + // an authority-less file: URL gets the same path check as one carrying an authority + assertFalse(urlValidator.isValid("file:///../../etc/passwd")); + assertFalse(urlValidator.isValid("file:/../../etc/passwd")); + assertFalse(urlValidator.isValid("file://localhost/../../etc/passwd")); + + // percent-encoded form of the same, as covered for http in testValidator383 + assertFalse(urlValidator.isValid("file:///..%2f..%2fetc/passwd")); + assertFalse(urlValidator.isValid("file:///%2e%2e/etc/passwd")); + + // an opaque file: URI has no path at all, just as http:example.com has none + assertFalse(urlValidator.isValid("file:etc/passwd")); + + assertTrue(urlValidator.isValid("file:///etc/hosts")); + assertTrue(urlValidator.isValid("file:/C:/path/to/dir/")); + } + + @Test + void testFileSchemePathOptions() { + final String[] schemes = { "file" }; + + final UrlValidator noDoubleSlashes = new UrlValidator(schemes, UrlValidator.ALLOW_LOCAL_URLS); + assertFalse(noDoubleSlashes.isValid("file:///tmp/a//b")); + assertFalse(noDoubleSlashes.isValid("file://localhost/tmp/a//b")); + + final UrlValidator allowDoubleSlashes = new UrlValidator(schemes, UrlValidator.ALLOW_LOCAL_URLS | UrlValidator.ALLOW_2_SLASHES); + assertTrue(allowDoubleSlashes.isValid("file:///tmp/a//b")); + + final UrlValidator noFragments = new UrlValidator(schemes, UrlValidator.ALLOW_LOCAL_URLS | UrlValidator.NO_FRAGMENTS); + assertFalse(noFragments.isValid("file:///tmp/x#frag")); + assertFalse(noFragments.isValid("file://localhost/tmp/x#frag")); + + assertTrue(noDoubleSlashes.isValid("file:///tmp/x#frag")); + } + @Test void testFragments() { final String[] schemes = { "http", "https" };