This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 69e3ead753 Improved: Validate the FTP connection target hostname in
sendContentToFtp before connecting
69e3ead753 is described below
commit 69e3ead75379181e7c80b741b13389b467d4b279
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 9144168208..8491de6ade 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
@@ -222,14 +222,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);
@@ -242,19 +261,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) {
@@ -262,28 +281,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;
@@ -298,7 +317,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");
}
}
}