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

mridulpathak 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 3ea43ad70f Improved: Validate the FTP connection target hostname in 
sendContentToFtp before connecting
3ea43ad70f is described below

commit 3ea43ad70f8259f6f5dff9a2807e6327b66b9664
Author: Mridul Pathak <[email protected]>
AuthorDate: Tue Oct 13 12:37:31 2026 +0530

    Improved: Validate the FTP connection target hostname in sendContentToFtp 
before connecting
    
    Adds UtilURL.checkHostResourceAllowed(String), a bare-hostname counterpart 
to the
    existing checkUrlResourceAllowed(URL) check, and applies it in
    FtpServices.sendContentToFtp before the outbound FTP connection is 
established.
    Rejects loopback, link-local, private, and other reserved targets, bringing 
the FTP
    delivery path in line with the validation already enforced on the 
URL_RESOURCE read
    path.
---
 .../org/apache/ofbiz/content/ftp/FtpServices.java  |  6 +++
 .../java/org/apache/ofbiz/base/util/UtilURL.java   | 47 +++++++++++++++-------
 2 files changed, 39 insertions(+), 14 deletions(-)

diff --git 
a/applications/content/src/main/java/org/apache/ofbiz/content/ftp/FtpServices.java
 
b/applications/content/src/main/java/org/apache/ofbiz/content/ftp/FtpServices.java
index f250dacb5a..5be08700dd 100644
--- 
a/applications/content/src/main/java/org/apache/ofbiz/content/ftp/FtpServices.java
+++ 
b/applications/content/src/main/java/org/apache/ofbiz/content/ftp/FtpServices.java
@@ -23,6 +23,7 @@ import org.apache.ofbiz.base.util.FileUtil;
 import org.apache.ofbiz.base.util.GeneralException;
 import org.apache.ofbiz.base.util.UtilMisc;
 import org.apache.ofbiz.base.util.UtilProperties;
+import org.apache.ofbiz.base.util.UtilURL;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.content.data.DataResourceWorker;
 import org.apache.ofbiz.entity.Delegator;
@@ -125,6 +126,11 @@ public class FtpServices {
             String serverType = hostname.split("://")[0];
             hostname = hostname.split("://")[1];
 
+            // SSRF protection: reject loopback, link-local, private, and 
other reserved
+            // targets, matching the equivalent check already enforced on the 
URL_RESOURCE read
+            // path (UtilURL.checkUrlResourceAllowed).
+            UtilURL.checkHostResourceAllowed(hostname);
+
             ftpClient = createFtpClient(serverType);
             if (null == ftpClient) {
                 return ServiceUtil.returnError("Server type : " + serverType + 
", not supported for hostname " + hostname);
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilURL.java 
b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilURL.java
index 2d9c111d59..0d19001314 100644
--- a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilURL.java
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilURL.java
@@ -220,14 +220,33 @@ public final class UtilURL {
             }
         }
 
+        checkHostResourceAllowed(host);
+    }
+
+    /**
+     * Throws {@link GeneralException} unless {@code host} resolves only to 
publicly routable
+     * addresses (not loopback, link-local, private, or otherwise reserved). 
Unlike
+     * {@link #checkUrlResourceAllowed(URL)}, this takes a bare hostname 
rather than an http/https
+     * URL, so it is usable by callers that connect over another protocol 
(e.g. FTP) but still need
+     * to validate a server-stored or otherwise untrusted connection target 
before dereferencing it,
+     * to avoid server-side request forgery against the local file system, 
loopback services, and
+     * the internal network.
+     *
+     * @throws GeneralException if the host is empty, cannot be resolved, or 
resolves to a
+     *      disallowed address
+     */
+    public static void checkHostResourceAllowed(String host) throws 
GeneralException {
+        if (UtilValidate.isEmpty(host)) {
+            throw new GeneralException("Host is empty");
+        }
         InetAddress[] addresses;
         try {
             addresses = InetAddress.getAllByName(host);
         } catch (UnknownHostException e) {
-            throw new GeneralException("URL host cannot be resolved: " + host);
+            throw new GeneralException("Host cannot be resolved: " + host);
         }
         if (addresses.length == 0) {
-            throw new GeneralException("URL host resolved to no addresses: " + 
host);
+            throw new GeneralException("Host resolved to no addresses: " + 
host);
         }
         for (InetAddress addr : addresses) {
             checkNotPrivateOrReservedAddress(addr);
@@ -240,19 +259,19 @@ public final class UtilURL {
      */
     private static void checkNotPrivateOrReservedAddress(InetAddress addr) 
throws GeneralException {
         if (addr.isLoopbackAddress()) {
-            throw new GeneralException("URL target resolves to a loopback 
address: " + addr.getHostAddress());
+            throw new GeneralException("Target resolves to a loopback address: 
" + addr.getHostAddress());
         }
         if (addr.isLinkLocalAddress()) {
-            throw new GeneralException("URL target resolves to a link-local 
address: " + addr.getHostAddress());
+            throw new GeneralException("Target resolves to a link-local 
address: " + addr.getHostAddress());
         }
         if (addr.isSiteLocalAddress()) {
-            throw new GeneralException("URL target resolves to a private 
(site-local) address: " + addr.getHostAddress());
+            throw new GeneralException("Target resolves to a private 
(site-local) address: " + addr.getHostAddress());
         }
         if (addr.isAnyLocalAddress()) {
-            throw new GeneralException("URL target resolves to a wildcard 
address: " + addr.getHostAddress());
+            throw new GeneralException("Target resolves to a wildcard address: 
" + addr.getHostAddress());
         }
         if (addr.isMulticastAddress()) {
-            throw new GeneralException("URL target resolves to a multicast 
address: " + addr.getHostAddress());
+            throw new GeneralException("Target resolves to a multicast 
address: " + addr.getHostAddress());
         }
         byte[] b = addr.getAddress();
         if (addr instanceof Inet4Address) {
@@ -260,28 +279,28 @@ public final class UtilURL {
             int i1 = b[1] & 0xFF;
             // 0.0.0.0/8 - "this" network (RFC 1122)
             if (i0 == 0) {
-                throw new GeneralException("URL target resolves to a reserved 
network address (0.0.0.0/8): " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to a reserved 
network address (0.0.0.0/8): " + addr.getHostAddress());
             }
             // 100.64.0.0/10 - shared address space / CGNAT (RFC 6598)
             if (i0 == 100 && i1 >= 64 && i1 <= 127) {
-                throw new GeneralException("URL target resolves to a shared 
address space (CGNAT, 100.64.0.0/10): " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to a shared 
address space (CGNAT, 100.64.0.0/10): " + addr.getHostAddress());
             }
             // 192.0.0.0/24 - IETF protocol assignments (RFC 6890)
             if (i0 == 192 && i1 == 0 && (b[2] & 0xFF) == 0) {
-                throw new GeneralException("URL target resolves to an IETF 
reserved address (192.0.0.0/24): " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to an IETF 
reserved address (192.0.0.0/24): " + addr.getHostAddress());
             }
             // 198.18.0.0/15 - network benchmarking (RFC 2544)
             if (i0 == 198 && (i1 == 18 || i1 == 19)) {
-                throw new GeneralException("URL target resolves to a 
benchmarking address (198.18.0.0/15): " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to a benchmarking 
address (198.18.0.0/15): " + addr.getHostAddress());
             }
             // 240.0.0.0/4 - reserved for future use (RFC 1112)
             if ((i0 & 0xF0) == 240) {
-                throw new GeneralException("URL target resolves to a reserved 
address (240.0.0.0/4): " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to a reserved 
address (240.0.0.0/4): " + addr.getHostAddress());
             }
         } else if (addr instanceof Inet6Address) {
             // fc00::/7 - Unique Local Addresses (ULA), private IPv6 (RFC 4193)
             if ((b[0] & 0xFE) == 0xFC) {
-                throw new GeneralException("URL target resolves to a 
unique-local (private) IPv6 address: " + addr.getHostAddress());
+                throw new GeneralException("Target resolves to a unique-local 
(private) IPv6 address: " + addr.getHostAddress());
             }
             // ::ffff:0:0/96 - IPv4-mapped IPv6; re-validate the embedded IPv4 
address
             boolean isIpv4Mapped = true;
@@ -296,7 +315,7 @@ public final class UtilURL {
                     checkNotPrivateOrReservedAddress(
                             InetAddress.getByAddress(new byte[]{b[12], b[13], 
b[14], b[15]}));
                 } catch (UnknownHostException e) {
-                    throw new GeneralException("URL target contains an invalid 
IPv4-mapped IPv6 address");
+                    throw new GeneralException("Target contains an invalid 
IPv4-mapped IPv6 address");
                 }
             }
         }

Reply via email to