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));
     }

Reply via email to