This is an automated email from the ASF dual-hosted git repository.
fuweng11 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git
The following commit(s) were added to refs/heads/master by this push:
new d04087bde0 [INLONG-12129][Manager] Fix the security vulnerability in
/api/node/testConnection (#12130)
d04087bde0 is described below
commit d04087bde0cf7694ff691ff34d3291fa08e9b02b
Author: fuweng11 <[email protected]>
AuthorDate: Mon Jun 1 15:31:54 2026 +0800
[INLONG-12129][Manager] Fix the security vulnerability in
/api/node/testConnection (#12130)
Co-authored-by: wakefu <[email protected]>
---
.../manager/common/util/UrlVerificationUtils.java | 106 +++++++++++++++++++++
.../manager/service/node/DataNodeServiceImpl.java | 12 +++
.../manager/web/controller/DataNodeController.java | 4 +
3 files changed, 122 insertions(+)
diff --git
a/inlong-manager/manager-common/src/main/java/org/apache/inlong/manager/common/util/UrlVerificationUtils.java
b/inlong-manager/manager-common/src/main/java/org/apache/inlong/manager/common/util/UrlVerificationUtils.java
index 086a73002f..e7f3814c02 100644
---
a/inlong-manager/manager-common/src/main/java/org/apache/inlong/manager/common/util/UrlVerificationUtils.java
+++
b/inlong-manager/manager-common/src/main/java/org/apache/inlong/manager/common/util/UrlVerificationUtils.java
@@ -19,6 +19,10 @@ package org.apache.inlong.manager.common.util;
import org.apache.inlong.manager.common.consts.InlongConstants;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+
public class UrlVerificationUtils {
/**
@@ -45,6 +49,7 @@ public class UrlVerificationUtils {
throw new Exception("Invalid host:port format in JDBC URL");
}
+ String host = hostPortSplit[0];
String portStr = hostPortSplit[1];
try {
int portNumber = Integer.parseInt(portStr);
@@ -54,5 +59,106 @@ public class UrlVerificationUtils {
} catch (NumberFormatException e) {
throw new Exception("Invalid port number format in JDBC URL");
}
+
+ // Validate host is not a private/internal address
+ validateHostNotInternal(host);
+ }
+
+ /**
+ * Validates that the given URL does not target internal/private network
addresses.
+ * This method prevents SSRF attacks by blocking requests to:
+ * - Loopback addresses (127.0.0.0/8, ::1)
+ * - Link-local addresses (169.254.0.0/16, fe80::/10)
+ * - Private network addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
+ * - Any address (0.0.0.0)
+ *
+ * @param url The URL string to validate (can be a full URL or host:port)
+ * @throws Exception If the URL targets an internal address or cannot be
resolved
+ */
+ public static void validateUrlNotInternal(String url) throws Exception {
+ if (url == null || url.trim().isEmpty()) {
+ throw new Exception("URL cannot be null or empty");
+ }
+
+ String host;
+ try {
+ // Try parsing as a full URI first
+ if (url.contains("://")) {
+ URI uri = new URI(url);
+ host = uri.getHost();
+ } else {
+ // Treat as host:port
+ String[] parts = url.split(InlongConstants.COLON);
+ host = parts[0];
+ }
+ } catch (Exception e) {
+ throw new Exception("Invalid URL format: " + url);
+ }
+
+ if (host == null || host.trim().isEmpty()) {
+ throw new Exception("Unable to extract host from URL: " + url);
+ }
+
+ validateHostNotInternal(host);
+ }
+
+ /**
+ * Validates that a hostname does not resolve to an internal/private IP
address.
+ *
+ * @param host The hostname or IP address to validate
+ * @throws Exception If the host resolves to an internal address
+ */
+ public static void validateHostNotInternal(String host) throws Exception {
+ if (host == null || host.trim().isEmpty()) {
+ throw new Exception("Host cannot be null or empty");
+ }
+
+ InetAddress[] addresses;
+ try {
+ addresses = InetAddress.getAllByName(host);
+ } catch (UnknownHostException e) {
+ throw new Exception("Unable to resolve host: " + host);
+ }
+
+ for (InetAddress address : addresses) {
+ if (isInternalAddress(address)) {
+ throw new Exception(
+ "Connection to internal/private network addresses is
not allowed: " + host);
+ }
+ }
+ }
+
+ /**
+ * Checks if an InetAddress is an internal/private network address.
+ *
+ * @param address The address to check
+ * @return true if the address is internal/private
+ */
+ private static boolean isInternalAddress(InetAddress address) {
+ return address.isLoopbackAddress()
+ || address.isLinkLocalAddress()
+ || address.isSiteLocalAddress()
+ || address.isAnyLocalAddress()
+ || address.isMulticastAddress()
+ || isCloudMetadataAddress(address);
+ }
+
+ /**
+ * Checks if the address is a well-known cloud metadata service address.
+ * AWS/GCP/Azure metadata service: 169.254.169.254
+ *
+ * @param address The address to check
+ * @return true if the address is a cloud metadata address
+ */
+ private static boolean isCloudMetadataAddress(InetAddress address) {
+ byte[] addr = address.getAddress();
+ // Check for 169.254.169.254 (cloud metadata endpoint)
+ if (addr.length == 4) {
+ return (addr[0] & 0xFF) == 169
+ && (addr[1] & 0xFF) == 254
+ && (addr[2] & 0xFF) == 169
+ && (addr[3] & 0xFF) == 254;
+ }
+ return false;
}
}
diff --git
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/node/DataNodeServiceImpl.java
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/node/DataNodeServiceImpl.java
index f7db3ec87e..9dc833f8eb 100644
---
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/node/DataNodeServiceImpl.java
+++
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/node/DataNodeServiceImpl.java
@@ -23,6 +23,7 @@ import
org.apache.inlong.manager.common.enums.TenantUserTypeEnum;
import org.apache.inlong.manager.common.exceptions.BusinessException;
import org.apache.inlong.manager.common.util.CommonBeanUtils;
import org.apache.inlong.manager.common.util.Preconditions;
+import org.apache.inlong.manager.common.util.UrlVerificationUtils;
import org.apache.inlong.manager.dao.entity.DataNodeEntity;
import org.apache.inlong.manager.dao.mapper.DataNodeEntityMapper;
import org.apache.inlong.manager.pojo.common.PageResult;
@@ -289,6 +290,17 @@ public class DataNodeServiceImpl implements
DataNodeService {
public Boolean testConnection(DataNodeRequest request) {
LOGGER.info("begin test connection for: {}", request);
+ // Validate URL to prevent SSRF attacks
+ String url = request.getUrl();
+ if (StringUtils.isNotBlank(url)) {
+ try {
+ UrlVerificationUtils.validateUrlNotInternal(url);
+ } catch (Exception e) {
+ throw new BusinessException(ErrorCodeEnum.INVALID_PARAMETER,
+ "SSRF protection: " + e.getMessage());
+ }
+ }
+
// according to the data node type, test connection
DataNodeOperator dataNodeOperator =
operatorFactory.getInstance(request.getType());
Boolean result = dataNodeOperator.testConnection(request);
diff --git
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/DataNodeController.java
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/DataNodeController.java
index 3cc71b0b72..4fb5487abc 100644
---
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/DataNodeController.java
+++
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/DataNodeController.java
@@ -30,6 +30,7 @@ import org.apache.inlong.manager.pojo.node.DataNodeInfo;
import org.apache.inlong.manager.pojo.node.DataNodePageRequest;
import org.apache.inlong.manager.pojo.node.DataNodeRequest;
import org.apache.inlong.manager.pojo.user.LoginUserUtils;
+import org.apache.inlong.manager.pojo.user.UserRoleCode;
import org.apache.inlong.manager.service.node.DataNodeService;
import org.apache.inlong.manager.service.operationlog.OperationLog;
@@ -37,6 +38,8 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
+import org.apache.shiro.authz.annotation.Logical;
+import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -134,6 +137,7 @@ public class DataNodeController {
@PostMapping("/node/testConnection")
@ApiOperation(value = "Test connection for data node")
+ @RequiresRoles(logical = Logical.OR, value = {UserRoleCode.INLONG_ADMIN,
UserRoleCode.TENANT_ADMIN})
public Response<Boolean> testConnection(@Validated @RequestBody
DataNodeRequest request) {
return Response.success(dataNodeService.testConnection(request));
}