This is an automated email from the ASF dual-hosted git repository.

ashishvijaywargiya pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/release24.09 by this push:
     new a76610be8e Restrict widget resource locations to component:// or an 
explicit allowlist (#1650) (#1654)
a76610be8e is described below

commit a76610be8eea9e80fde944a9762b1f5b9df9d1be
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Fri Aug 14 21:34:06 2026 +0530

    Restrict widget resource locations to component:// or an explicit allowlist 
(#1650) (#1654)
---
 .../commonext/widget/ofbizsetup/ProfileScreens.xml |  4 +-
 .../org/apache/ofbiz/base/util/UtilValidate.java   | 26 +++++----
 framework/security/config/security.properties      |  9 ++++
 .../java/org/apache/ofbiz/webapp/WebAppUtil.java   | 10 ++++
 .../org/apache/ofbiz/widget/model/FormFactory.java | 19 +++++--
 .../org/apache/ofbiz/widget/model/GridFactory.java | 19 +++++--
 .../org/apache/ofbiz/widget/model/MenuFactory.java | 17 +++++-
 .../apache/ofbiz/widget/model/ScreenFactory.java   |  2 +-
 .../org/apache/ofbiz/widget/model/TreeFactory.java |  9 +++-
 .../ofbiz/widget/model/WidgetSecureLocation.java   | 61 ++++++++++++++++++----
 10 files changed, 141 insertions(+), 35 deletions(-)

diff --git a/applications/commonext/widget/ofbizsetup/ProfileScreens.xml 
b/applications/commonext/widget/ofbizsetup/ProfileScreens.xml
index a2a5ae7232..e0269bd4db 100644
--- a/applications/commonext/widget/ofbizsetup/ProfileScreens.xml
+++ b/applications/commonext/widget/ofbizsetup/ProfileScreens.xml
@@ -90,8 +90,8 @@
             <set field="helpAnchor" 
value="_help_for_view_organization_profile"/>
           </actions>
             <widgets>
-                <include-screen name="Party" 
location="applications/party/widget/partymgr/ProfileScreens.xml"/>
-                <include-screen name="Contact" 
location="applications/party/widget/partymgr/ProfileScreens.xml"/>
+                <include-screen name="Party" 
location="component://party/widget/partymgr/ProfileScreens.xml"/>
+                <include-screen name="Contact" 
location="component://party/widget/partymgr/ProfileScreens.xml"/>
             </widgets>
         </section>
     </screen>
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilValidate.java 
b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilValidate.java
index 3feda0787e..dba02b2bb0 100644
--- a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilValidate.java
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilValidate.java
@@ -18,11 +18,11 @@
  
*******************************************************************************/
 package org.apache.ofbiz.base.util;
 
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 import org.apache.commons.validator.routines.EmailValidator;
 import org.apache.commons.validator.routines.UrlValidator;
@@ -155,9 +155,6 @@ public final class UtilValidate {
     public static final String CONTIGUOUS_US_STATE_CODES = 
"AL|AZ|AR|CA|CO|CT|DE|DC|FL|GA|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|"
             + "NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY";
 
-    /** Paths from which loading files should be prevented */
-    public static final String[] BLOCKED_PATHS = {"proc/self/fd"};
-
     /** Check whether an object is empty, will see if it is a String, Map, 
Collection, etc. */
     public static boolean isEmpty(Object o) {
         return ObjectType.isEmpty(o);
@@ -662,22 +659,23 @@ public final class UtilValidate {
     }
 
     /**
-     * isBlockedPath takes a String representing a filePath, normalizes it and 
checks it against a Blacklist
+     * isAllowedPath takes a String representing a non-component widget 
resource path, normalizes it and
+     * checks it against the administrator-configured 
<code>security.allowFilePaths</code> regular
+     * expression. Unset or blank configuration denies every path (secure by 
default): an administrator
+     * must explicitly opt in to loading widget resources from outside a 
<code>component://</code> location.
      * @param rawPathString
-     * @return true if its a blocked path, false otherwise or if it is empty
+     * @return true if it's an allowed path, false otherwise (including when 
unconfigured)
      */
-    public static boolean isBlockedPath(String rawPathString) {
+    public static boolean isAllowedPath(String rawPathString) {
         if (UtilValidate.isEmpty(rawPathString)) {
             return false;
         }
-        Path normalized = Paths.get(rawPathString).normalize();
-        String normalizedPath = normalized.toString();
-        for (String blocked : BLOCKED_PATHS) {
-            if (normalizedPath.contains(blocked)) {
-                return true;
-            }
+        String allowFilePaths = UtilProperties.getPropertyValue("security", 
"allowFilePaths", "");
+        if (UtilValidate.isEmpty(allowFilePaths)) {
+            return false;
         }
-        return false;
+        String normalizedPath = 
Paths.get(rawPathString).normalize().toString();
+        return 
Pattern.compile(allowFilePaths).matcher(normalizedPath).matches();
     }
 
     /** isYear returns true if string s is a valid
diff --git a/framework/security/config/security.properties 
b/framework/security/config/security.properties
index f70ec1d597..7ec6b9f93a 100644
--- a/framework/security/config/security.properties
+++ b/framework/security/config/security.properties
@@ -300,6 +300,15 @@ 
deniedFileExtensions=html,htm,php,php1,php2,hph3,php4,php5,php6,php7,phps,asp,as
 #-- As it name says, allowAllUploads opens all possibilities
 allowAllUploads=
 
+#--
+#-- Widget resources (screens, forms, grids, menus, trees) are only loaded 
from a component://
+#-- location by default. allowFilePaths is a regular expression an 
administrator can set to also
+#-- allow loading widget resources from bare filesystem paths outside any 
component, matched via
+#-- UtilValidate::isAllowedPath. Left blank (the default), every non-component 
location is denied.
+#-- A file: URI (in any letter case, e.g. file:/some/path) is never allowed 
here, regardless of
+#-- this setting: see WidgetSecureLocation.
+allowFilePaths=
+
 #--
 #-- Default characters that are allowed in file names and file extensions to 
guarantee safeness
 #-- Uncomment to change. Note that allowing all characters is at risk.
diff --git 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/WebAppUtil.java 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/WebAppUtil.java
index 80cbfe50f1..07f87a7b9d 100644
--- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/WebAppUtil.java
+++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/WebAppUtil.java
@@ -159,8 +159,18 @@ public final class WebAppUtil {
             Debug.logWarning(ioe, MODULE);
         }
         if (requestBodyMap != null) {
+            ServletContext servletContext = request.getServletContext();
             Set<String> parameterNames = requestBodyMap.keySet();
             for (String parameterName: parameterNames) {
+                // A request body is anonymous, attacker-controlled input. 
Never let it shadow a name
+                // the webapp already exposes as a trusted, application-owned 
ServletContext attribute
+                // (e.g. mainDecoratorLocation, set from web.xml at filter 
init) - doing so let an
+                // unauthenticated JSON request redirect trusted widget/screen 
locations.
+                if (servletContext.getAttribute(parameterName) != null) {
+                    Debug.logWarning("Ignoring request body attribute [%s]: it 
shadows an existing"
+                            + " ServletContext attribute of the same name", 
MODULE, parameterName);
+                    continue;
+                }
                 request.setAttribute(parameterName, 
requestBodyMap.get(parameterName));
             }
         }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/FormFactory.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/FormFactory.java
index 80753d4d07..8987407dcc 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/FormFactory.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/FormFactory.java
@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.ofbiz.base.location.FlexibleLocation;
+import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.UtilXml;
@@ -71,7 +72,13 @@ public class FormFactory {
         String cacheKey = sb.toString();
         ModelForm modelForm = FORM_LOCATION_CACHE.get(cacheKey);
         if (modelForm == null) {
-            URL formFileUrl = FlexibleLocation.resolveLocation(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of form [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, formName, resourceName);
+                throw new IllegalArgumentException("Abort form rendering due 
to unallowed form location");
+            }
+            URL formFileUrl = 
FlexibleLocation.resolveLocation(sanitizedLocation);
             if (formFileUrl == null || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(formFileUrl.toString()))
 {
                 throw new IllegalArgumentException("Could not resolve location 
to URL: " + resourceName);
             }
@@ -104,11 +111,17 @@ public class FormFactory {
         if (modelForm == null) {
             Delegator delegator = (Delegator) 
request.getAttribute("delegator");
             LocalDispatcher dispatcher = (LocalDispatcher) 
request.getAttribute("dispatcher");
-            URL formFileUrl = 
request.getServletContext().getResource(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of form [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, formName, resourceName);
+                throw new IllegalArgumentException("Abort form rendering due 
to unallowed form location");
+            }
+            URL formFileUrl = 
request.getServletContext().getResource(sanitizedLocation);
             Document formFileDoc = UtilXml.readXmlDocument(formFileUrl, true, 
true);
             Element formElement = 
UtilXml.firstChildElement(formFileDoc.getDocumentElement(), "form", "name", 
formName);
             modelForm = createModelForm(formElement, 
delegator.getModelReader(), visualTheme, dispatcher.getDispatchContext(),
-                    resourceName, formName);
+                    sanitizedLocation, formName);
             modelForm = FORM_WEBAPP_CACHE.putIfAbsentAndGet(cacheKey, 
modelForm);
         }
         if (modelForm == null) {
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/GridFactory.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/GridFactory.java
index a615114ad6..febc4b18f1 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/GridFactory.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/GridFactory.java
@@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.ofbiz.base.location.FlexibleLocation;
+import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.UtilXml;
@@ -73,7 +74,13 @@ public class GridFactory {
         String cacheKey = sb.toString();
         ModelGrid modelGrid = GRID_LOCATION_CACHE.get(cacheKey);
         if (modelGrid == null) {
-            URL gridFileUrl = FlexibleLocation.resolveLocation(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of grid [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, gridName, resourceName);
+                throw new IllegalArgumentException("Abort grid rendering due 
to unallowed grid location");
+            }
+            URL gridFileUrl = 
FlexibleLocation.resolveLocation(sanitizedLocation);
             if (gridFileUrl == null || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(gridFileUrl.toString()))
 {
                 throw new IllegalArgumentException("Could not resolve location 
to URL: " + resourceName);
             }
@@ -108,11 +115,17 @@ public class GridFactory {
             ServletContext servletContext = request.getServletContext();
             Delegator delegator = (Delegator) 
request.getAttribute("delegator");
             LocalDispatcher dispatcher = (LocalDispatcher) 
request.getAttribute("dispatcher");
-            URL gridFileUrl = servletContext.getResource(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of grid [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, gridName, resourceName);
+                throw new IllegalArgumentException("Abort grid rendering due 
to unallowed grid location");
+            }
+            URL gridFileUrl = servletContext.getResource(sanitizedLocation);
             Document gridFileDoc = UtilXml.readXmlDocument(gridFileUrl, true, 
true);
             Element gridElement = 
UtilXml.firstChildElement(gridFileDoc.getDocumentElement(), "grid", "name", 
gridName);
             modelGrid = createModelGrid(gridElement, 
delegator.getModelReader(), visualTheme,
-                    dispatcher.getDispatchContext(), resourceName, gridName);
+                    dispatcher.getDispatchContext(), sanitizedLocation, 
gridName);
             modelGrid = GRID_WEBAPP_CACHE.putIfAbsentAndGet(cacheKey, 
modelGrid);
         }
         if (modelGrid == null) {
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/MenuFactory.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/MenuFactory.java
index 91455398cf..0e176a0fc5 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/MenuFactory.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/MenuFactory.java
@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.ofbiz.base.location.FlexibleLocation;
+import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.UtilXml;
@@ -65,7 +66,13 @@ public class MenuFactory {
         if (modelMenuMap == null) {
             ServletContext servletContext = request.getServletContext();
 
-            URL menuFileUrl = servletContext.getResource(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of menu [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, menuName, resourceName);
+                throw new IllegalArgumentException("Abort menu rendering due 
to unallowed menu location");
+            }
+            URL menuFileUrl = servletContext.getResource(sanitizedLocation);
             Document menuFileDoc = UtilXml.readXmlDocument(menuFileUrl, true, 
true);
             modelMenuMap = readMenuDocument(menuFileDoc, location, 
visualTheme);
             MENU_WEBAPP_CACHE.putIfAbsent(cacheKey, modelMenuMap);
@@ -106,7 +113,13 @@ public class MenuFactory {
         String keyName = resourceName + "::" + visualTheme.getVisualThemeId();
         Map<String, ModelMenu> modelMenuMap = MENU_LOCATION_CACHE.get(keyName);
         if (modelMenuMap == null) {
-            URL menuFileUrl = FlexibleLocation.resolveLocation(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of menu [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, menuName, resourceName);
+                throw new IllegalArgumentException("Abort menu rendering due 
to unallowed menu location");
+            }
+            URL menuFileUrl = 
FlexibleLocation.resolveLocation(sanitizedLocation);
             if (menuFileUrl == null || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(menuFileUrl.toString()))
 {
                 throw new IllegalArgumentException("Could not resolve location 
to URL: " + resourceName);
             }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScreenFactory.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScreenFactory.java
index 8e0537721c..157a7328b1 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScreenFactory.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScreenFactory.java
@@ -199,7 +199,7 @@ public class ScreenFactory {
         if (UtilValidate.isNotEmpty(location)) {
             String sanitizedLocation = WidgetSecureLocation.sanitize(location);
             if (sanitizedLocation == null) {
-                Debug.logWarning("The location of screen [%s] isn't an allowed 
Path. Abort rendering. Raw location [%s]", MODULE, name, location);
+                Debug.logWarning("The location of screen [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]", MODULE, name, location);
                 throw new IllegalArgumentException("Abort screen rendering due 
to unallowed screen location");
             }
             try {
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/TreeFactory.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/TreeFactory.java
index c32e381741..c58d531dfa 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/TreeFactory.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/TreeFactory.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.ofbiz.base.location.FlexibleLocation;
+import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.UtilXml;
 import org.apache.ofbiz.base.util.cache.UtilCache;
@@ -50,7 +51,13 @@ public class TreeFactory {
             throws IOException, SAXException, ParserConfigurationException {
         Map<String, ModelTree> modelTreeMap = 
TREE_LOCATION_CACHE.get(resourceName);
         if (modelTreeMap == null) {
-            URL treeFileUrl = FlexibleLocation.resolveLocation(resourceName);
+            String sanitizedLocation = 
WidgetSecureLocation.sanitize(resourceName);
+            if (sanitizedLocation == null) {
+                Debug.logWarning("The location of tree [%s] isn't an allowed 
path. Abort rendering. Raw location [%s]",
+                        MODULE, treeName, resourceName);
+                throw new IllegalArgumentException("Abort tree rendering due 
to unallowed tree location");
+            }
+            URL treeFileUrl = 
FlexibleLocation.resolveLocation(sanitizedLocation);
             if (treeFileUrl == null || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(treeFileUrl.toString()))
 {
                 throw new IllegalArgumentException("Could not resolve location 
to URL: " + resourceName);
             }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/WidgetSecureLocation.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/WidgetSecureLocation.java
index 8c87b7b640..16a6e2f85e 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/WidgetSecureLocation.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/WidgetSecureLocation.java
@@ -19,29 +19,72 @@
 package org.apache.ofbiz.widget.model;
 
 import java.nio.file.Paths;
+
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilValidate;
 
+/**
+ * Central gatekeeper for every widget XML resource location (screen, form, 
grid, menu, tree) before
+ * it is resolved and parsed. A widget location can be influenced by request 
context (for example a
+ * decorator's {@code location} attribute is commonly a Flexible String 
Expression such as
+ * {@code ${parameters.mainDecoratorLocation}}), so this class treats every 
location as untrusted and
+ * only allows it through when it is either:
+ * <ul>
+ *   <li>a {@code component://} location with no {@code ..} traversal segment, 
or</li>
+ *   <li>a bare filesystem path matched by the administrator-configured
+ *       {@code security.allowFilePaths} allowlist (denied by default).</li>
+ * </ul>
+ * Any {@code file:} scheme location - single-slash or otherwise, in any 
letter case - is rejected
+ * outright at the protocol layer: it is never a legitimate widget location, 
and on Linux it is the
+ * carrier used to reach live process-descriptor aliases such as {@code 
file:/dev/fd/N} or
+ * {@code file:/proc/thread-self/fd/N}.
+ */
 public final class WidgetSecureLocation {
 
     private static final String MODULE = WidgetSecureLocation.class.getName();
-    private static final String COMPO_TYPE = "component://";
+    private static final String COMPONENT_PROTOCOL = "component://";
+
+    private WidgetSecureLocation() { }
 
+    /**
+     * Sanitizes a widget resource location.
+     * @param location the raw, potentially untrusted location
+     * @return the (possibly normalized) location if it is allowed, or {@code 
null} if it must be rejected
+     */
     public static String sanitize(String location) {
-        if (UtilValidate.isEmpty(location)
-                || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(location)
-                || location.startsWith("file:/")) {
+        if (UtilValidate.isEmpty(location)) {
+            Debug.logWarning("Unable to sanitize an empty widget location", 
MODULE);
+            return null;
+        }
+        if (isFileScheme(location) || 
UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(location)) {
             Debug.logWarning(String.format("Unable to sanitize location: 
[%s]", location), MODULE);
             return null;
         }
-        if (location.startsWith(COMPO_TYPE) && location.length() > 12) {
-            if (location.indexOf("..") > 0) {
-                Debug.logWarning(String.format("For security reason traversal 
sequence '..' is not allowed: [%s]", location), MODULE);
+        if (location.startsWith(COMPONENT_PROTOCOL)) {
+            String componentRelativePath = 
location.substring(COMPONENT_PROTOCOL.length());
+            if (componentRelativePath.contains("..")) {
+                Debug.logWarning(String.format("Traversal sequence '..' is not 
allowed in location: [%s]", location), MODULE);
                 return null;
             }
-            return COMPO_TYPE + Paths.get(location.substring(12)).normalize();
+            return COMPONENT_PROTOCOL + 
Paths.get(componentRelativePath).normalize();
         }
+        if (UtilValidate.isAllowedPath(location)) {
+            return location;
+        }
+        Debug.logWarning(String.format("Location isn't on the configured 
allowed file path: [%s]", location), MODULE);
+        return null;
+    }
 
-        return location.startsWith(COMPO_TYPE) ? location : null;
+    /**
+     * Detects the {@code file:} URI scheme regardless of case or slash count, 
e.g. {@code file:/},
+     * {@code File:/}, {@code FILE://}. Java's URL/{@code FlexibleLocation} 
scheme resolution is
+     * case-insensitive, so this check must be too.
+     */
+    private static boolean isFileScheme(String location) {
+        int colonIndex = location.indexOf(':');
+        if (colonIndex < 0) {
+            return false;
+        }
+        return "file".equalsIgnoreCase(location.substring(0, colonIndex));
     }
 }

Reply via email to