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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 63ed16cf9b7 branch-4.1: [fix](jdbc) Harden JDBC driver URL validation 
and remove file-upload HTTP API (#67149)
63ed16cf9b7 is described below

commit 63ed16cf9b73394f5508ecd9dba76cebea07f4bf
Author: Calvin Kirs <[email protected]>
AuthorDate: Wed Aug 26 22:56:36 2026 +0800

    branch-4.1: [fix](jdbc) Harden JDBC driver URL validation and remove 
file-upload HTTP API (#67149)
    
    #65987
---
 .../main/java/org/apache/doris/common/Config.java  |  48 ++--
 .../java/org/apache/doris/common/ConfigTest.java   |  26 +-
 .../org/apache/doris/catalog/JdbcResource.java     | 133 ++++++++-
 .../doris/datasource/jdbc/JdbcExternalCatalog.java |  37 +++
 .../org/apache/doris/httpv2/rest/UploadAction.java | 318 ---------------------
 .../apache/doris/httpv2/util/LoadSubmitter.java    | 179 ------------
 .../org/apache/doris/httpv2/util/TmpFileMgr.java   | 307 --------------------
 .../org/apache/doris/catalog/JdbcResourceTest.java | 166 +++++++++++
 .../datasource/jdbc/JdbcExternalCatalogTest.java   |  24 ++
 9 files changed, 395 insertions(+), 843 deletions(-)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index 618dd936daf..126b29998eb 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -170,12 +170,13 @@ public class Config extends ConfigBase {
                     + "if the specified driver file path is not an absolute 
path, Doris will find jars from this path"})
     public static String jdbc_drivers_dir = EnvUtils.getDorisHome() + 
"/plugins/jdbc_drivers";
 
-    @ConfField(description = {"JDBC 驱动的安全路径。在创建 JDBC Catalog 
时,允许使用的文件或者网络路径,可配置多个,使用分号分隔"
-            + "默认为 * 表示全部允许,如果设置为空也表示全部允许",
-            "The safe path of the JDBC driver. When creating a JDBC Catalog,"
-                    + "you can configure multiple files or network paths that 
are allowed to be used,"
-                    + "separated by semicolons"
-                    + "The default is * to allow all, if set to empty, also 
means to allow all"})
+    @ConfField(description = {
+            "JDBC 驱动的安全路径。在创建 JDBC Catalog 时,允许使用的文件或者网络路径,可配置多个,使用分号分隔。"
+                    + "默认为 * 
表示全部允许,如果设置为空也表示全部允许。配置具体路径时会按路径组件匹配,拒绝路径遍历和前缀混淆。",
+            "The safe path of the JDBC driver. When creating a JDBC Catalog, 
you can configure multiple "
+                    + "allowed file or network paths separated by semicolons. 
The default is * to allow all; "
+                    + "if set to empty, it also means to allow all. When set 
to concrete paths, driver URLs "
+                    + "are matched structurally, so path traversal and prefix 
confusion are rejected."})
     public static String jdbc_driver_secure_path = "*";
 
     @ConfField(description = {"MySQL Jdbc Catalog mysql 不支持下推的函数",
@@ -192,8 +193,12 @@ public class Config extends ConfigBase {
                     + "these variables, it just needs to accept them without 
error."})
     public static String[] mysql_compat_var_whitelist = {};
 
-    @ConfField(mutable = true, masterOnly = true, description = {"强制 SQLServer 
Jdbc Catalog 加密为 false",
-            "Force SQLServer Jdbc Catalog encrypt to false"})
+    @ConfField(description = {
+            "强制 SQLServer Jdbc Catalog 加密为 false。该配置会禁用 SQLServer JDBC 传输加密,"
+                    + "因此只能通过 fe.conf 设置,不能在运行时修改。",
+            "Force SQLServer Jdbc Catalog encrypt to false. This is a 
security-sensitive switch that disables "
+                    + "SQLServer JDBC transport encryption, so it can only be 
set in fe.conf and is not "
+                    + "modifiable at runtime via ADMIN SET FRONTEND CONFIG."})
     public static boolean force_sqlserver_jdbc_encrypt_false = false;
 
     @ConfField(mutable = true, masterOnly = true, description = {"broker load 
时,单个节点上 load 执行计划的默认并行度",
@@ -3216,12 +3221,6 @@ public class Config extends ConfigBase {
     })
     public static int http_sql_submitter_max_worker_threads = 2;
 
-    @ConfField(mutable = false, masterOnly = false, description = {
-        "http 请求处理/api/upload 任务的最大线程池。",
-        "The max number work threads of http upload submitter."
-    })
-    public static int http_load_submitter_max_worker_threads = 2;
-
     @ConfField(mutable = true, masterOnly = true, description = {
             "load label 个数阈值,超过该个数后,对于已经完成导入作业或者任务,"
             + "其 label 会被删除,被删除的 label 可以被重用。值为 -1 时,表示此阈值不生效。",
@@ -3617,9 +3616,13 @@ public class Config extends ConfigBase {
     @ConfField(mutable = true)
     public static int mow_calculate_delete_bitmap_retry_times = 10;
 
-    @ConfField(mutable = true, description = {"指定 S3 Load endpoint 
白名单,举例:s3_load_endpoint_white_list=a,b,c",
-            "the white list for the s3 load endpoint, if it is empty, no white 
list will be set,"
-            + "for example: s3_load_endpoint_white_list=a,b,c"})
+    @ConfField(description = {
+            "指定 S3 Load endpoint 白名单。如果为空,则不设置白名单。只能通过 fe.conf 设置,并在重启后生效。"
+                    + "例如:s3_load_endpoint_white_list=a,b,c。",
+            "The allowlist for S3 load endpoints. If it is empty, no allowlist 
will be set. "
+                    + "For example: s3_load_endpoint_white_list=a,b,c. "
+                    + "This can only be set in fe.conf and takes effect after 
a restart; "
+                    + "it is intentionally not modifiable at runtime via ADMIN 
SET FRONTEND CONFIG."})
     public static String[] s3_load_endpoint_white_list = {};
 
     @ConfField(mutable = true, description = {
@@ -3665,10 +3668,13 @@ public class Config extends ConfigBase {
             ".dfs.core.cloudapi.de"
     };
 
-    @ConfField(mutable = true, description = {"指定 Jdbc driver url 
白名单,举例:jdbc_driver_url_white_list=a,b,c",
-            "the white list for jdbc driver url, if it is empty, no white list 
will be set"
-            + "for example: jdbc_driver_url_white_list=a,b,c"
-    })
+    @ConfField(description = {
+            "指定 JDBC driver URL 白名单。如果为空,则不设置白名单。只能通过 fe.conf 设置,并在重启后生效。"
+                    + "例如:jdbc_driver_url_white_list=a,b,c。",
+            "The allowlist for JDBC driver URLs. If it is empty, no allowlist 
will be set. "
+                    + "For example: jdbc_driver_url_white_list=a,b,c. "
+                    + "This can only be set in fe.conf and takes effect after 
a restart; "
+                    + "it is intentionally not modifiable at runtime via ADMIN 
SET FRONTEND CONFIG."})
     public static String[] jdbc_driver_url_white_list = {};
 
     @ConfField(description = {"Stream_Load 导入时,label 被限制的最大长度",
diff --git a/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java 
b/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
index d48d7ae8ed3..c59cb44c66d 100644
--- a/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
+++ b/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
@@ -94,8 +94,28 @@ public class ConfigTest {
 
     @Test
     public void testSetEmptyArray() throws ConfigException {
-        ConfigBase.setMutableConfig("s3_load_endpoint_white_list", "a,b,c");
-        ConfigBase.setMutableConfig("s3_load_endpoint_white_list", "");
-        Assert.assertEquals("array length should be 0", 0, 
Config.s3_load_endpoint_white_list.length);
+        ConfigBase.setMutableConfig("mysql_compat_var_whitelist", "a,b,c");
+        ConfigBase.setMutableConfig("mysql_compat_var_whitelist", "");
+        Assert.assertEquals("array length should be 0", 0, 
Config.mysql_compat_var_whitelist.length);
+    }
+
+    // File-path and jdbc-driver security configs must only be settable in 
fe.conf (ops), never at runtime
+    // via ADMIN SET FRONTEND CONFIG. setMutableConfig is exactly that runtime 
entrypoint, so it must reject them.
+    @Test
+    public void testSecurityPathConfigsAreNotRuntimeMutable() {
+        String[] opsOnlyConfigs = {
+                "jdbc_driver_url_white_list",
+                "jdbc_drivers_dir",
+                "jdbc_driver_secure_path",
+                "tmp_dir",
+                "plugin_dir",
+                "s3_load_endpoint_white_list",
+                "force_sqlserver_jdbc_encrypt_false",
+        };
+        for (String key : opsOnlyConfigs) {
+            ConfigException e = Assert.assertThrows(key + " should not be 
runtime-mutable",
+                    ConfigException.class, () -> 
ConfigBase.setMutableConfig(key, "x"));
+            Assert.assertTrue(e.getMessage().contains("is not mutable"));
+        }
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
index f1e312faebe..b5d720609fc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
@@ -46,12 +46,15 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * External JDBC Catalog resource for external table query.
@@ -304,28 +307,128 @@ public class JdbcResource extends Resource {
                     + "file://xxx.jar, http://xxx.jar, https://xxx.jar, or 
xxx.jar (without prefix).");
         }
 
+        URI uri;
         try {
-            URI uri = new URI(driverUrl);
-            String schema = uri.getScheme();
-            checkCloudWhiteList(driverUrl);
-            if (schema == null && !driverUrl.startsWith("/")) {
-                return checkAndReturnDefaultDriverUrl(driverUrl);
-            }
+            uri = new URI(driverUrl);
+        } catch (URISyntaxException e) {
+            // Fail closed: an unparsable URL must never be silently accepted, 
otherwise the
+            // allowed-path check below could be bypassed by a malformed URL.
+            LOG.warn("invalid jdbc driver url: {}", driverUrl, e);
+            throw new IllegalArgumentException("Invalid driver URL: " + 
driverUrl);
+        }
+
+        String schema = uri.getScheme();
+        checkCloudWhiteList(driverUrl);
+        if (schema == null && !driverUrl.startsWith("/")) {
+            // A scheme-less driver_url is a plain jar file name resolved 
under jdbc_drivers_dir. This
+            // shared resolver is also on the lazy load path of pre-existing 
catalogs (Iceberg/Paimon/
+            // legacy JDBC consumers call it directly, with no create/alter or 
replay context), so it
+            // deliberately applies no new restriction here: an unmodified 
historical catalog must keep
+            // resolving exactly as before. The mandatory bare-name grammar is 
enforced only when a
+            // catalog is created or altered, in 
JdbcExternalCatalog.checkDriverUrlSecurityRule.
+            return checkAndReturnDefaultDriverUrl(driverUrl);
+        }
+
+        // "*" or an empty/blank value means allow all (the documented, 
backward-compatible contract).
+        String securePath = Config.jdbc_driver_secure_path;
+        if (securePath == null || securePath.trim().isEmpty() || 
"*".equals(securePath.trim())) {
+            return driverUrl;
+        }
 
-            if ("*".equals(Config.jdbc_driver_secure_path)) {
-                return driverUrl;
+        if (!isDriverUrlAllowed(driverUrl, uri)) {
+            throw new IllegalArgumentException("Driver URL does not match any 
allowed paths: " + driverUrl);
+        }
+        return driverUrl;
+    }
+
+    /**
+     * Check whether {@code driverUrl} falls under one of the 
semicolon-separated prefixes configured in
+     * {@link Config#jdbc_driver_secure_path}. Matching is structural 
(component-based) rather than a raw string
+     * prefix, so that neither prefix confusion ({@code /opt/drivers} vs 
{@code /opt/drivers-evil}) nor path
+     * traversal ({@code /opt/drivers/../etc}) can slip a driver outside the 
allowed location.
+     */
+    private static boolean isDriverUrlAllowed(String driverUrl, URI uri) {
+        String scheme = uri.getScheme();
+        List<String> allowedPaths = new ArrayList<>();
+        for (String p : Config.jdbc_driver_secure_path.split(";")) {
+            String trimmed = p.trim();
+            if (!trimmed.isEmpty()) {
+                allowedPaths.add(trimmed);
             }
+        }
+        if ("http".equalsIgnoreCase(scheme) || 
"https".equalsIgnoreCase(scheme)) {
+            URI candidate = uri.normalize();
+            return allowedPaths.stream().anyMatch(allowed -> 
remoteUrlMatches(candidate, allowed));
+        }
+        // Only file:// reaches here; bare absolute paths and bare "*.jar" are 
handled earlier.
+        // A local file URL must carry no authority, query or fragment. 
Otherwise validation (which
+        // looks only at URI.getPath()) and the consumers (URLClassLoader / 
checksum, which act on the
+        // whole original URL) would address different objects — e.g. 
"file://attacker/dir/x.jar" is
+        // fetched from a remote authority, and "file:///dir/x.jar?evil" maps 
to a sibling file.
+        String authority = uri.getRawAuthority();
+        if ((authority != null && !authority.isEmpty())
+                || uri.getRawQuery() != null || uri.getRawFragment() != null) {
+            return false;
+        }
+        Path candidate = toLocalPath(driverUrl).normalize();
+        return allowedPaths.stream()
+                .map(allowed -> toLocalPath(allowed).normalize())
+                .anyMatch(candidate::startsWith);
+    }
 
-            boolean isAllowed = 
Arrays.stream(Config.jdbc_driver_secure_path.split(";"))
-                    .anyMatch(allowedPath -> 
driverUrl.startsWith(allowedPath.trim()));
-            if (!isAllowed) {
-                throw new IllegalArgumentException("Driver URL does not match 
any allowed paths: " + driverUrl);
+    /**
+     * Turn a {@code file://} URL or a plain filesystem path into a {@link 
Path} for structural comparison.
+     * A {@code file://} URL is decoded exactly once via {@link URI#getPath()} 
so that percent-encoded
+     * segments (e.g. {@code %2e%2e}) are resolved into the same 
representation the driver-loading
+     * consumers ({@code URL.openStream} / {@code URLClassLoader}) will use; 
otherwise an encoded parent
+     * segment would survive normalization and escape the allowed directory.
+     */
+    private static Path toLocalPath(String pathOrUrl) {
+        if (pathOrUrl.startsWith("file:")) {
+            try {
+                String decoded = new URI(pathOrUrl).getPath();
+                if (decoded != null && !decoded.isEmpty()) {
+                    return Paths.get(decoded);
+                }
+            } catch (URISyntaxException ignored) {
+                // fall through to literal stripping below
             }
-            return driverUrl;
+            int sep = pathOrUrl.indexOf("//");
+            return Paths.get(sep >= 0 ? pathOrUrl.substring(sep + 2) : 
pathOrUrl.substring("file:".length()));
+        }
+        return Paths.get(pathOrUrl);
+    }
+
+    /**
+     * Structural match for remote (http/https) driver URLs: scheme, host and 
port must be equal, and the
+     * candidate path must sit under the allowed path (component-based). A 
bare path prefix (no scheme) can
+     * never authorize a remote URL.
+     */
+    private static boolean remoteUrlMatches(URI candidate, String allowedPath) 
{
+        URI base;
+        try {
+            base = new URI(allowedPath).normalize();
         } catch (URISyntaxException e) {
-            LOG.warn("invalid jdbc driver url: " + driverUrl);
-            return driverUrl;
+            return false;
         }
+        if (base.getScheme() == null) {
+            return false;
+        }
+        // Scheme/host/port and the path prefix must match, and the 
resource-selecting components
+        // (user-info and query) that the checksum/classloader consumers act 
on must match exactly too,
+        // otherwise e.g. ".../download?id=approved" would authorize 
".../download?id=evil".
+        return base.getScheme().equalsIgnoreCase(candidate.getScheme())
+                && base.getHost() != null && 
base.getHost().equalsIgnoreCase(candidate.getHost())
+                && base.getPort() == candidate.getPort()
+                && Objects.equals(base.getUserInfo(), candidate.getUserInfo())
+                && Objects.equals(base.getRawQuery(), candidate.getRawQuery())
+                && pathIsUnder(candidate.getPath(), base.getPath());
+    }
+
+    private static boolean pathIsUnder(String candidatePath, String basePath) {
+        Path candidate = Paths.get(candidatePath == null || 
candidatePath.isEmpty() ? "/" : candidatePath).normalize();
+        Path base = Paths.get(basePath == null || basePath.isEmpty() ? "/" : 
basePath).normalize();
+        return candidate.startsWith(base);
     }
 
     private static String checkAndReturnDefaultDriverUrl(String driverUrl) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
index a841626bcad..74f84577720 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
@@ -60,13 +60,17 @@ import org.apache.thrift.TException;
 import org.apache.thrift.TSerializer;
 
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.regex.Pattern;
 
 public class JdbcExternalCatalog extends ExternalCatalog {
     private static final Logger LOG = 
LogManager.getLogger(JdbcExternalCatalog.class);
+    private static final Pattern SAFE_DRIVER_FILE_NAME = 
Pattern.compile("^[A-Za-z0-9._-]+\\.jar$");
 
     private static final List<String> REQUIRED_PROPERTIES = ImmutableList.of(
             JdbcResource.JDBC_URL,
@@ -103,6 +107,7 @@ public class JdbcExternalCatalog extends ExternalCatalog {
                 throw new DdlException("Required property '" + 
requiredProperty + "' is missing");
             }
         }
+        
checkDriverUrlSecurityRule(catalogProperty.getProperties().get(JdbcResource.DRIVER_URL));
 
         
JdbcResource.checkBooleanProperty(JdbcResource.ONLY_SPECIFIED_DATABASE, 
getOnlySpecifiedDatabase());
         
JdbcResource.checkBooleanProperty(ExternalCatalog.LOWER_CASE_META_NAMES, 
getLowerCaseMetaNames());
@@ -118,6 +123,38 @@ public class JdbcExternalCatalog extends ExternalCatalog {
         
ExternalFunctionRules.check(catalogProperty.getProperties().getOrDefault(JdbcResource.FUNCTION_RULES,
 ""));
     }
 
+    /**
+     * Enforce the mandatory driver URL rule on user-facing CREATE and ALTER 
CATALOG validation.
+     * Catalog replay does not call {@link #checkProperties()}, so existing 
catalogs remain compatible.
+     */
+    static void checkDriverUrlSecurityRule(String driverUrl) throws 
DdlException {
+        if (driverUrl == null || driverUrl.isEmpty()) {
+            return;
+        }
+        String pathToCheck = driverUrl;
+        if (driverUrl.contains("://")) {
+            try {
+                String decoded = new URI(driverUrl).getPath();
+                if (decoded != null) {
+                    pathToCheck = decoded;
+                }
+            } catch (URISyntaxException e) {
+                throw new DdlException("Invalid driver_url: " + driverUrl);
+            }
+        }
+        String probe = pathToCheck.replace('\\', '/');
+        for (String segment : probe.split("/")) {
+            if ("..".equals(segment)) {
+                throw new DdlException(
+                        "Invalid driver_url: path traversal ('..') is not 
allowed: " + driverUrl);
+            }
+        }
+        if (!driverUrl.contains("://") && 
!SAFE_DRIVER_FILE_NAME.matcher(driverUrl).matches()) {
+            throw new DdlException("Invalid driver_url: a driver file name 
must match "
+                    + "[A-Za-z0-9._-]+.jar (got: " + driverUrl + ")");
+        }
+    }
+
     @Override
     public void setDefaultPropsIfMissing(boolean isReplay) {
         super.setDefaultPropsIfMissing(isReplay);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/UploadAction.java 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/UploadAction.java
deleted file mode 100644
index fa00e4a5ab3..00000000000
--- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/UploadAction.java
+++ /dev/null
@@ -1,318 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package org.apache.doris.httpv2.rest;
-
-import org.apache.doris.common.Config;
-import org.apache.doris.httpv2.entity.ResponseEntityBuilder;
-import org.apache.doris.httpv2.util.LoadSubmitter;
-import org.apache.doris.httpv2.util.TmpFileMgr;
-import org.apache.doris.mysql.privilege.PrivPredicate;
-import org.apache.doris.qe.ConnectContext;
-import org.apache.doris.system.SystemInfoService;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-/**
- * Upload file
- */
-@RestController
-public class UploadAction extends RestBaseController {
-    private static final Logger LOG = LogManager.getLogger(UploadAction.class);
-    private static TmpFileMgr fileMgr = new TmpFileMgr(Config.tmp_dir);
-    private static LoadSubmitter loadSubmitter = new LoadSubmitter();
-
-    private static final String PARAM_COLUMN_SEPARATOR = "column_separator";
-    private static final String PARAM_PREVIEW = "preview";
-    private static final String PARAM_FILE_ID = "file_id";
-    private static final String PARAM_FILE_UUID = "file_uuid";
-
-    /**
-     * Upload the file
-     * @param ns
-     * @param dbName
-     * @param tblName
-     * @param file
-     * @param request
-     * @param response
-     * @return
-     */
-    @RequestMapping(path = "/api/{" + NS_KEY + "}/{" + DB_KEY + "}/{" + 
TABLE_KEY + "}/upload",
-            method = {RequestMethod.POST})
-    public Object upload(
-            @PathVariable(value = NS_KEY) String ns,
-            @PathVariable(value = DB_KEY) String dbName,
-            @PathVariable(value = TABLE_KEY) String tblName,
-            @RequestParam("file") MultipartFile file,
-            HttpServletRequest request, HttpServletResponse response) {
-        if (needRedirect(request.getScheme())) {
-            return redirectToHttps(request);
-        }
-
-        checkWithCookie(request, response, false);
-
-        if (!ns.equalsIgnoreCase(SystemInfoService.DEFAULT_CLUSTER)) {
-            return ResponseEntityBuilder.badRequest("Only support 
'default_cluster' now");
-        }
-
-        checkTblAuth(ConnectContext.get().getCurrentUserIdentity(), dbName, 
tblName, PrivPredicate.LOAD);
-
-        String columnSeparator = request.getParameter(PARAM_COLUMN_SEPARATOR);
-        if (Strings.isNullOrEmpty(columnSeparator)) {
-            columnSeparator = "\t";
-        }
-
-        String preview = request.getParameter(PARAM_PREVIEW);
-        if (Strings.isNullOrEmpty(preview)) {
-            preview = "false"; // default is false
-        }
-
-        if (file.isEmpty()) {
-            return ResponseEntityBuilder.badRequest("Empty file");
-        }
-
-        try {
-            TmpFileMgr.TmpFile tmpFile = fileMgr.upload(new 
TmpFileMgr.UploadFile(file, columnSeparator));
-            TmpFileMgr.TmpFile copiedFile = tmpFile.copy();
-            if (preview.equalsIgnoreCase("true")) {
-                copiedFile.setPreview();
-            }
-            return ResponseEntityBuilder.ok(copiedFile);
-        } catch (TmpFileMgr.TmpFileException | IOException e) {
-            return ResponseEntityBuilder.okWithCommonError(e.getMessage());
-        }
-    }
-
-    /**
-     * Load the uploaded file
-     * @param ns
-     * @param dbName
-     * @param tblName
-     * @param request
-     * @param response
-     * @return
-     */
-    @RequestMapping(path = "/api/{" + NS_KEY + "}/{" + DB_KEY + "}/{" + 
TABLE_KEY + "}/upload",
-            method = {RequestMethod.PUT})
-    public Object submit(
-            @PathVariable(value = NS_KEY) String ns,
-            @PathVariable(value = DB_KEY) String dbName,
-            @PathVariable(value = TABLE_KEY) String tblName,
-            HttpServletRequest request, HttpServletResponse response) {
-
-        // This is a strict restriction
-        if (!Strings.isNullOrEmpty(Config.security_checker_class_name)) {
-            return ResponseEntityBuilder.badRequest("Not support upload data 
api in security env");
-        }
-
-        ActionAuthorizationInfo authInfo = checkWithCookie(request, response, 
false);
-
-        if (!ns.equalsIgnoreCase(SystemInfoService.DEFAULT_CLUSTER)) {
-            return ResponseEntityBuilder.badRequest("Only support 
'default_cluster' now");
-        }
-
-        checkTblAuth(ConnectContext.get().getCurrentUserIdentity(), dbName, 
tblName, PrivPredicate.LOAD);
-
-        String fileIdStr = request.getParameter(PARAM_FILE_ID);
-        if (Strings.isNullOrEmpty(fileIdStr)) {
-            return ResponseEntityBuilder.badRequest("Missing file id 
parameter");
-        }
-        String fileUUIDStr = request.getParameter(PARAM_FILE_UUID);
-        if (Strings.isNullOrEmpty(fileUUIDStr)) {
-            return ResponseEntityBuilder.badRequest("Missing file id 
parameter");
-        }
-
-        TmpFileMgr.TmpFile tmpFile = null;
-        try {
-            tmpFile = fileMgr.getFile(Long.valueOf(fileIdStr), fileUUIDStr);
-        } catch (TmpFileMgr.TmpFileException e) {
-            return ResponseEntityBuilder.okWithCommonError("file not found");
-        }
-        Preconditions.checkNotNull(tmpFile, fileIdStr);
-
-        LoadContext loadContext = new LoadContext(request, dbName, tblName,
-                authInfo.fullUserName, authInfo.password, tmpFile);
-        Future<LoadSubmitter.SubmitResult> future = 
loadSubmitter.submit(loadContext);
-
-        try {
-            LoadSubmitter.SubmitResult res = future.get();
-            return ResponseEntityBuilder.ok(res);
-        } catch (InterruptedException | ExecutionException e) {
-            return ResponseEntityBuilder.okWithCommonError(e.getMessage());
-        }
-    }
-
-    /**
-     * Get all uploaded file or specified file
-     * If preview is true, also return the preview of the file
-     * @param ns
-     * @param dbName
-     * @param tblName
-     * @param request
-     * @param response
-     * @return
-     */
-    @RequestMapping(path = "/api/{" + NS_KEY + "}/{" + DB_KEY + "}/{" + 
TABLE_KEY + "}/upload",
-            method = {RequestMethod.GET})
-    public Object list(
-            @PathVariable(value = NS_KEY) String ns,
-            @PathVariable(value = DB_KEY) String dbName,
-            @PathVariable(value = TABLE_KEY) String tblName,
-            HttpServletRequest request, HttpServletResponse response) {
-
-        checkWithCookie(request, response, false);
-
-        if (!ns.equalsIgnoreCase(SystemInfoService.DEFAULT_CLUSTER)) {
-            return ResponseEntityBuilder.badRequest("Only support 
'default_cluster' now");
-        }
-
-        checkTblAuth(ConnectContext.get().getCurrentUserIdentity(), dbName, 
tblName, PrivPredicate.LOAD);
-
-        String fileIdStr = request.getParameter(PARAM_FILE_ID);
-        String fileUUIDStr = request.getParameter(PARAM_FILE_UUID);
-
-        if (Strings.isNullOrEmpty(fileIdStr) || 
Strings.isNullOrEmpty(fileUUIDStr)) {
-            // not specified file id, return all files list
-            List<TmpFileMgr.TmpFileBrief> files = fileMgr.listFiles();
-            return ResponseEntityBuilder.ok(files);
-        }
-
-        // return specified file
-        String preview = request.getParameter(PARAM_PREVIEW);
-        if (Strings.isNullOrEmpty(preview)) {
-            preview = "true"; // default is true
-        }
-
-        try {
-            TmpFileMgr.TmpFile tmpFile = 
fileMgr.getFile(Long.valueOf(fileIdStr), fileUUIDStr);
-            TmpFileMgr.TmpFile copiedFile = tmpFile.copy();
-            if (preview.equalsIgnoreCase("true")) {
-                copiedFile.setPreview();
-            }
-            return ResponseEntityBuilder.ok(copiedFile);
-        } catch (TmpFileMgr.TmpFileException | IOException e) {
-            return ResponseEntityBuilder.okWithCommonError(e.getMessage());
-        }
-    }
-
-    @RequestMapping(path = "/api/{" + NS_KEY + "}/{" + DB_KEY + "}/{" + 
TABLE_KEY + "}/upload",
-            method = {RequestMethod.DELETE})
-    public Object delete(
-            @PathVariable(value = NS_KEY) String ns,
-            @PathVariable(value = DB_KEY) String dbName,
-            @PathVariable(value = TABLE_KEY) String tblName,
-            HttpServletRequest request, HttpServletResponse response) {
-
-        checkWithCookie(request, response, false);
-
-        if (!ns.equalsIgnoreCase(SystemInfoService.DEFAULT_CLUSTER)) {
-            return ResponseEntityBuilder.badRequest("Only support 
'default_cluster' now");
-        }
-
-        checkTblAuth(ConnectContext.get().getCurrentUserIdentity(), dbName, 
tblName, PrivPredicate.LOAD);
-
-        String fileIdStr = request.getParameter(PARAM_FILE_ID);
-        if (Strings.isNullOrEmpty(fileIdStr)) {
-            return ResponseEntityBuilder.badRequest("Missing file id 
parameter");
-        }
-        String fileUUIDStr = request.getParameter(PARAM_FILE_UUID);
-        if (Strings.isNullOrEmpty(fileUUIDStr)) {
-            return ResponseEntityBuilder.badRequest("Missing file id 
parameter");
-        }
-
-        fileMgr.deleteFile(Long.valueOf(fileIdStr), fileUUIDStr);
-        return ResponseEntityBuilder.ok();
-    }
-
-    /**
-     * A context to save infos of stream load
-     */
-    public static class LoadContext {
-        public String user;
-        public String passwd;
-        public String db;
-        public String tbl;
-        public TmpFileMgr.TmpFile file;
-
-        public String label;
-        public String columnSeparator;
-        public String columns;
-        public String where;
-        public String maxFilterRatio;
-        public String partitions;
-        public String timeout;
-        public String strictMode;
-        public String timezone;
-        public String execMemLimit;
-        public String format;
-        public String jsonPaths;
-        public String stripOuterArray;
-        public String jsonRoot;
-        public String numAsString;
-        public String fuzzyParse;
-
-
-        public LoadContext(HttpServletRequest request, String db,
-                String tbl, String user, String passwd, TmpFileMgr.TmpFile 
file) {
-            this.db = db;
-            this.tbl = tbl;
-            this.user = user;
-            this.passwd = passwd;
-            this.file = file;
-
-            parseHeader(request);
-        }
-
-        private void parseHeader(HttpServletRequest request) {
-            this.label = request.getHeader("label");
-            this.columnSeparator = file.columnSeparator;
-            if (!Strings.isNullOrEmpty(request.getHeader("column_separator"))) 
{
-                this.columnSeparator = request.getHeader("column_separator");
-            }
-            this.columns = request.getHeader("columns");
-            this.where = request.getHeader("where");
-            this.maxFilterRatio = request.getHeader("max_filter_ratio");
-            this.partitions = request.getHeader("partitions");
-            this.timeout = request.getHeader("timeout");
-            this.strictMode = request.getHeader("strict_mode");
-            this.timezone = request.getHeader("timezone");
-            this.execMemLimit = request.getHeader("exec_mem_limit");
-            this.format = request.getHeader("format");
-            this.jsonPaths = request.getHeader("jsonpaths");
-            this.stripOuterArray = request.getHeader("strip_outer_array");
-            this.numAsString = request.getHeader("num_as_string");
-            this.jsonRoot = request.getHeader("json_root");
-            this.fuzzyParse = request.getHeader("fuzzy_parse");
-        }
-    }
-}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/LoadSubmitter.java 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/LoadSubmitter.java
deleted file mode 100644
index fc5f85743bd..00000000000
--- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/LoadSubmitter.java
+++ /dev/null
@@ -1,179 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package org.apache.doris.httpv2.util;
-
-import org.apache.doris.catalog.Env;
-import org.apache.doris.cluster.ClusterNamespace;
-import org.apache.doris.common.Config;
-import org.apache.doris.common.LoadException;
-import org.apache.doris.common.ThreadPoolManager;
-import org.apache.doris.common.util.NetUtils;
-import org.apache.doris.httpv2.rest.UploadAction;
-import org.apache.doris.system.Backend;
-import org.apache.doris.system.BeSelectionPolicy;
-import org.apache.doris.system.SystemInfoService;
-
-import com.google.common.base.Strings;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.reflect.Type;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.ThreadPoolExecutor;
-
-public class LoadSubmitter {
-    private static final Logger LOG = 
LogManager.getLogger(LoadSubmitter.class);
-
-    private ThreadPoolExecutor executor = 
ThreadPoolManager.newDaemonCacheThreadPoolThrowException(
-                        Config.http_load_submitter_max_worker_threads, 
"load-submitter", true);
-
-    private static final String STREAM_LOAD_URL_PATTERN = 
"http://%s/api/%s/%s/_stream_load";;
-
-    public Future<SubmitResult> submit(UploadAction.LoadContext loadContext) {
-        LoadSubmitter.Worker worker = new LoadSubmitter.Worker(loadContext);
-        return executor.submit(worker);
-    }
-
-    private static class Worker implements Callable<SubmitResult> {
-
-        private UploadAction.LoadContext loadContext;
-
-        public Worker(UploadAction.LoadContext loadContext) {
-            this.loadContext = loadContext;
-        }
-
-        @Override
-        public SubmitResult call() throws Exception {
-            try {
-                return load();
-            } catch (Throwable e) {
-                LOG.warn("failed to submit load. label: {}", 
loadContext.label, e);
-                throw e;
-            }
-        }
-
-        private SubmitResult load() throws Exception {
-            // choose a backend to submit the stream load
-            Backend be = selectOneBackend();
-
-            String hostPort = 
NetUtils.getHostPortInAccessibleFormat(be.getHost(), be.getHttpPort());
-            String loadUrlStr = String.format(STREAM_LOAD_URL_PATTERN, 
hostPort, loadContext.db, loadContext.tbl);
-            URL loadUrl = new URL(loadUrlStr);
-            HttpURLConnection conn = (HttpURLConnection) 
loadUrl.openConnection();
-            conn.setRequestMethod("PUT");
-            String auth = String.format("%s:%s", 
ClusterNamespace.getNameFromFullName(loadContext.user),
-                    loadContext.passwd);
-            String authEncoding = 
Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
-            conn.setRequestProperty("Authorization", "Basic " + authEncoding);
-            conn.addRequestProperty("Expect", "100-continue");
-            conn.addRequestProperty("Content-Type", "text/plain; 
charset=UTF-8");
-            if (!Strings.isNullOrEmpty(loadContext.columns)) {
-                conn.addRequestProperty("columns", loadContext.columns);
-            }
-            if (!Strings.isNullOrEmpty(loadContext.columnSeparator)) {
-                conn.addRequestProperty("column_separator", 
loadContext.columnSeparator);
-            }
-            if (!Strings.isNullOrEmpty(loadContext.label)) {
-                conn.addRequestProperty("label", loadContext.label);
-            }
-            conn.setDoOutput(true);
-            conn.setDoInput(true);
-
-            File loadFile = checkAndGetFile(loadContext.file);
-            try (BufferedOutputStream bos = new 
BufferedOutputStream(conn.getOutputStream());
-                    BufferedInputStream bis = new BufferedInputStream(new 
FileInputStream(loadFile));) {
-                int i;
-                while ((i = bis.read()) > 0) {
-                    bos.write(i);
-                }
-            }
-
-            int status = conn.getResponseCode();
-            String respMsg = conn.getResponseMessage();
-
-            LOG.info("get status: {}, response msg: {}", status, respMsg);
-
-            InputStream stream = (InputStream) conn.getContent();
-            BufferedReader br = new BufferedReader(new 
InputStreamReader(stream));
-            StringBuilder sb = new StringBuilder();
-            String line;
-            while ((line = br.readLine()) != null) {
-                sb.append(line);
-            }
-            Type type = new TypeToken<SubmitResult>() {
-            }.getType();
-            SubmitResult result = new Gson().fromJson(sb.toString(), type);
-            return result;
-        }
-
-        private File checkAndGetFile(TmpFileMgr.TmpFile tmpFile) {
-            File file = new File(tmpFile.absPath);
-            return file;
-        }
-
-        private Backend selectOneBackend() throws LoadException {
-            BeSelectionPolicy policy = new 
BeSelectionPolicy.Builder().needLoadAvailable().build();
-            List<Long> backendIds = 
Env.getCurrentSystemInfo().selectBackendIdsByPolicy(policy, 1);
-            if (backendIds.isEmpty()) {
-                throw new 
LoadException(SystemInfoService.NO_BACKEND_LOAD_AVAILABLE_MSG + ", policy: " + 
policy);
-            }
-            Backend backend = 
Env.getCurrentSystemInfo().getBackend(backendIds.get(0));
-            if (backend == null) {
-                throw new 
LoadException(SystemInfoService.NO_BACKEND_LOAD_AVAILABLE_MSG + ", policy: " + 
policy);
-            }
-            return backend;
-        }
-    }
-
-    // CHECKSTYLE OFF: These name must match the name in json, case-sensitive.
-    public static class SubmitResult {
-        public String TxnId;
-        public String Label;
-        public String Status;
-        public String ExistingJobStatus;
-        public String Message;
-        public String NumberTotalRows;
-        public String NumberLoadedRows;
-        public String NumberFilteredRows;
-        public String NumberUnselectedRows;
-        public String LoadBytes;
-        public String LoadTimeMs;
-        public String BeginTxnTimeMs;
-        public String StreamLoadPutTimeMs;
-        public String ReadDataTimeMs;
-        public String WriteDataTimeMs;
-        public String CommitAndPublishTimeMs;
-        public String ErrorURL;
-    }
-    // CHECKSTYLE ON
-}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/TmpFileMgr.java 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/TmpFileMgr.java
deleted file mode 100644
index dcdbc4bac63..00000000000
--- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/TmpFileMgr.java
+++ /dev/null
@@ -1,307 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package org.apache.doris.httpv2.util;
-
-import org.apache.doris.common.util.Util;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.stream.Collectors;
-
-/**
- * Manager the file uploaded.
- * This file manager is currently only used to manage files
- * uploaded through the Upload RESTFul API.
- * And limit the number and size of the maximum upload file.
- * It can also browse or delete files through the RESTFul API.
- */
-public class TmpFileMgr {
-    public static final Logger LOG = LogManager.getLogger(TmpFileMgr.class);
-
-    private static final long MAX_TOTAL_FILE_SIZE_BYTES = 1 * 1024 * 1024 * 
1024L; // 1GB
-    private static final long MAX_TOTAL_FILE_NUM = 100;
-    public static final long MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024L; // 
100MB
-    private static final String UPLOAD_DIR = "_doris_upload";
-
-    private AtomicLong fileIdGenerator = new AtomicLong(0);
-    private String rootDir;
-    private Map<Long, TmpFile> fileMap = Maps.newConcurrentMap();
-
-    private long totalFileSize = 0;
-
-    public TmpFileMgr(String dir) {
-        this.rootDir = dir + "/" + UPLOAD_DIR;
-        init();
-    }
-
-    private void init() {
-        File root = new File(rootDir);
-        if (!root.exists()) {
-            root.mkdirs();
-        } else if (!root.isDirectory()) {
-            throw new IllegalStateException("Path " + rootDir + " is not 
directory");
-        }
-
-        // delete all files under this dir at startup.
-        // This means that all uploaded files will be lost after FE restarts.
-        // This is just for simplicity.
-        Util.deleteDirectory(root);
-        root.mkdirs();
-    }
-
-    /**
-     * Simply used `synchronized` to allow only one user upload file at one 
time.
-     * So that we can easily control the number of files and total size of 
files.
-     *
-     * @param uploadFile
-     * @return
-     * @throws TmpFileException
-     */
-    public synchronized TmpFile upload(UploadFile uploadFile) throws 
TmpFileException {
-        if (uploadFile.file.getSize() > MAX_SINGLE_FILE_SIZE) {
-            throw new TmpFileException("File size " + uploadFile.file.getSize()
-                    + " exceed limit " + MAX_SINGLE_FILE_SIZE);
-        }
-
-        if (totalFileSize + uploadFile.file.getSize() > 
MAX_TOTAL_FILE_SIZE_BYTES) {
-            throw new TmpFileException("Total file size will exceed limit " + 
MAX_TOTAL_FILE_SIZE_BYTES);
-        }
-
-        if (fileMap.size() > MAX_TOTAL_FILE_NUM) {
-            throw new TmpFileException("Number of temp file " + fileMap.size() 
+ " exceed limit " + MAX_TOTAL_FILE_NUM);
-        }
-
-        long fileId = fileIdGenerator.incrementAndGet();
-        String fileUUID = UUID.randomUUID().toString();
-
-        TmpFile tmpFile = new TmpFile(fileId, fileUUID, 
uploadFile.file.getOriginalFilename(),
-                uploadFile.file.getSize(), uploadFile.columnSeparator);
-        try {
-            tmpFile.save(uploadFile.file);
-        } catch (IOException e) {
-            throw new TmpFileException("Failed to upload file. Reason: " + 
e.getMessage());
-        }
-        fileMap.put(tmpFile.id, tmpFile);
-        totalFileSize += uploadFile.file.getSize();
-        return tmpFile;
-    }
-
-    public TmpFile getFile(long id, String uuid) throws TmpFileException {
-        TmpFile tmpFile = fileMap.get(id);
-        if (tmpFile == null || !tmpFile.uuid.equals(uuid)) {
-            throw new TmpFileException("File with [" + id + "-" + uuid + "] 
does not exist");
-        }
-        return tmpFile;
-    }
-
-    public List<TmpFileBrief> listFiles() {
-        return fileMap.values().stream().map(t -> new 
TmpFileBrief(t)).collect(Collectors.toList());
-    }
-
-    /**
-     * Delete the specified file and remove it from fileMap
-     * @param fileId
-     * @param fileUUID
-     */
-    public void deleteFile(Long fileId, String fileUUID) {
-        Iterator<Map.Entry<Long, TmpFile>> iterator = 
fileMap.entrySet().iterator();
-        while (iterator.hasNext()) {
-            Map.Entry<Long, TmpFile> entry = iterator.next();
-            if (entry.getValue().id == fileId && 
entry.getValue().uuid.equals(fileUUID)) {
-                entry.getValue().delete();
-                iterator.remove();
-            }
-        }
-        return;
-    }
-
-    public class TmpFile {
-        public final long id;
-        public final String uuid;
-        public final String originFileName;
-        public final long fileSize;
-        public String columnSeparator;
-        public String absPath;
-
-        public List<List<String>> lines = null;
-        public int maxColNum = 0;
-
-        private static final int MAX_PREVIEW_LINES = 10;
-
-        public TmpFile(long id, String uuid, String originFileName, long 
fileSize, String columnSeparator) {
-            this.id = id;
-            this.uuid = uuid;
-            this.originFileName = originFileName;
-            this.fileSize = fileSize;
-            this.columnSeparator = columnSeparator;
-        }
-
-        public void save(MultipartFile file) throws IOException {
-            File dest = new File(Joiner.on("/").join(rootDir, uuid));
-            boolean uploadSucceed = false;
-            try {
-                file.transferTo(dest);
-                this.absPath = dest.getAbsolutePath();
-                uploadSucceed = true;
-                LOG.info("upload file {} succeed at {}", this, 
dest.getAbsolutePath());
-            } catch (IOException e) {
-                LOG.warn("failed to upload file {}, dest: {}", this, 
dest.getAbsolutePath(), e);
-                throw e;
-            } finally {
-                if (!uploadSucceed) {
-                    dest.delete();
-                }
-            }
-        }
-
-        public void setPreview() throws IOException {
-            lines = Lists.newArrayList();
-            String escapedColSep = Util.escapeSingleRegex(columnSeparator);
-            try (FileReader fr = new FileReader(absPath);
-                    BufferedReader bf = new BufferedReader(fr)) {
-                String str;
-                while ((str = bf.readLine()) != null) {
-                    String[] cols = str.split(escapedColSep, -1); // -1 to 
keep the last empty column
-                    lines.add(Lists.newArrayList(cols));
-                    if (cols.length > maxColNum) {
-                        maxColNum = cols.length;
-                    }
-                    if (lines.size() >= MAX_PREVIEW_LINES) {
-                        break;
-                    }
-                }
-            }
-        }
-
-        // make a copy without lines and maxColNum.
-        // so that can call `setPreview` and will not affect other instance
-        public TmpFile copy() {
-            TmpFile copiedFile = new TmpFile(this.id, this.uuid, 
this.originFileName,
-                    this.fileSize, this.columnSeparator);
-            copiedFile.absPath = this.absPath;
-            return copiedFile;
-        }
-
-        public void delete() {
-            File file = new File(absPath);
-            file.delete();
-            LOG.info("delete tmp file: {}", this);
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            sb.append("[id=").append(id).append(", 
uuid=").append(uuid).append(", origin name=").append(originFileName)
-                    .append(", size=").append(fileSize).append("]");
-            return sb.toString();
-        }
-    }
-
-    // a brief of TmpFile.
-    // TODO(cmy): it can be removed by using Lombok's annotation in TmpFile 
class
-    public static class TmpFileBrief {
-        public long id;
-        public String uuid;
-        public String originFileName;
-        public long fileSize;
-        public String columnSeparator;
-
-        public TmpFileBrief(TmpFile tmpFile) {
-            this.id = tmpFile.id;
-            this.uuid = tmpFile.uuid;
-            this.originFileName = tmpFile.originFileName;
-            this.fileSize = tmpFile.fileSize;
-            this.columnSeparator = tmpFile.columnSeparator;
-        }
-
-        public long getId() {
-            return id;
-        }
-
-        public void setId(long id) {
-            this.id = id;
-        }
-
-        public String getUuid() {
-            return uuid;
-        }
-
-        public void setUuid(String uuid) {
-            this.uuid = uuid;
-        }
-
-        public String getOriginFileName() {
-            return originFileName;
-        }
-
-        public void setOriginFileName(String originFileName) {
-            this.originFileName = originFileName;
-        }
-
-        public long getFileSize() {
-            return fileSize;
-        }
-
-        public void setFileSize(long fileSize) {
-            this.fileSize = fileSize;
-        }
-
-        public String getColumnSeparator() {
-            return columnSeparator;
-        }
-
-        public void setColumnSeparator(String columnSeparator) {
-            this.columnSeparator = columnSeparator;
-        }
-    }
-
-
-    public static class UploadFile {
-        public MultipartFile file;
-        public String columnSeparator;
-
-        public UploadFile(MultipartFile file, String columnSeparator) {
-            this.file = file;
-            this.columnSeparator = columnSeparator;
-        }
-    }
-
-    public static class TmpFileException extends Exception {
-        public TmpFileException(String msg) {
-            super(msg);
-        }
-
-        public TmpFileException(String msg, Throwable t) {
-            super(msg, t);
-        }
-    }
-}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java
index 2e2d9af9626..6a8799d9030 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.catalog;
 
+import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.UserException;
@@ -246,4 +247,169 @@ public class JdbcResourceTest {
             JdbcResource.getFullDriverUrl(invalidUrl4);
         });
     }
+
+    @Test
+    public void testSecurePathRejectsPrefixConfusion() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            // A directory that merely shares a string prefix must NOT be 
allowed.
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("file:///opt/doris/jdbc_drivers-evil/x.jar"));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsPathTraversal() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("file:///opt/doris/jdbc_drivers/../../etc/x.jar"));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathAllowsPathUnderAllowedDir() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            String url = "file:///opt/doris/jdbc_drivers/sub/x.jar";
+            Assertions.assertDoesNotThrow(() -> Assert.assertEquals(url, 
JdbcResource.getFullDriverUrl(url)));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsHostConfusion() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "http://good.com/";;
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("http://good.com.evil.com/x.jar";));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathAllowsRemoteUnderAllowedHost() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "http://good.com/drivers";;
+            String url = "http://good.com/drivers/x.jar";;
+            Assertions.assertDoesNotThrow(() -> Assert.assertEquals(url, 
JdbcResource.getFullDriverUrl(url)));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathWildcardAllowsAll() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "*";
+            String url = "file:///any/where/x.jar";
+            Assertions.assertDoesNotThrow(() -> Assert.assertEquals(url, 
JdbcResource.getFullDriverUrl(url)));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsEncodedTraversal() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            // %2e%2e decodes to "..", which must be resolved the same way the 
classloader resolves it.
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("file:///opt/doris/jdbc_drivers/%2e%2e/%2e%2e/etc/x.jar"));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsRemoteQueryMismatch() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "http://good.com/drivers";;
+            // A query-bearing URL must not be authorized by a query-less 
allowed prefix.
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("http://good.com/drivers/x.jar?id=evil";));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsRemoteUserInfoMismatch() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "http://good.com/drivers";;
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("http://[email protected]/drivers/x.jar";));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSchemelessLegacyCharsAccepted() {
+        // The shared resolver is on the lazy load path of pre-existing 
catalogs, so it applies no new
+        // restriction: a bare name using historically valid characters (e.g. 
'+') must keep resolving,
+        // so an unmodified historical catalog is not broken after upgrade. 
The stricter bare-name
+        // grammar applies only when a catalog is created or altered (enforced 
by JdbcExternalCatalog).
+        String savedDir = Config.jdbc_drivers_dir;
+        try {
+            Config.jdbc_drivers_dir = "/opt/doris/jdbc_drivers";
+            
Assert.assertEquals("file:///opt/doris/jdbc_drivers/legacy+patched.jar",
+                    JdbcResource.getFullDriverUrl("legacy+patched.jar"));
+        } finally {
+            Config.jdbc_drivers_dir = savedDir;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsFileAuthority() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            // A non-local authority makes consumers fetch a remote object 
though the path matches.
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("file://attacker.example/opt/doris/jdbc_drivers/evil.jar"));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testSecurePathRejectsFileQuery() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            Config.jdbc_driver_secure_path = "file:///opt/doris/jdbc_drivers";
+            Assert.assertThrows(IllegalArgumentException.class, () ->
+                    
JdbcResource.getFullDriverUrl("file:///opt/doris/jdbc_drivers/x.jar?evil"));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
+
+    @Test
+    public void testEmptySecurePathAllowsAll() {
+        String saved = Config.jdbc_driver_secure_path;
+        try {
+            // Empty means allow-all, same as "*" (backward-compatible 
contract).
+            Config.jdbc_driver_secure_path = "";
+            String url = "file:///opt/doris/jdbc_drivers/x.jar";
+            Assertions.assertDoesNotThrow(() -> Assert.assertEquals(url, 
JdbcResource.getFullDriverUrl(url)));
+        } finally {
+            Config.jdbc_driver_secure_path = saved;
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalogTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalogTest.java
index c63aebfcb35..a0eb338c4b6 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalogTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalogTest.java
@@ -111,4 +111,28 @@ public class JdbcExternalCatalogTest {
                 exceptione3.getMessage());
 
     }
+
+    @Test
+    public void testDriverUrlSecurityRule() throws DdlException {
+        Assert.assertThrows(DdlException.class,
+                () -> 
JdbcExternalCatalog.checkDriverUrlSecurityRule("../evil.jar"));
+        Assert.assertThrows(DdlException.class,
+                () -> 
JdbcExternalCatalog.checkDriverUrlSecurityRule("sub/dir/driver.jar"));
+        Assert.assertThrows(DdlException.class,
+                () -> 
JdbcExternalCatalog.checkDriverUrlSecurityRule("driver.jar; rm -rf /"));
+        Assert.assertThrows(DdlException.class,
+                () -> JdbcExternalCatalog.checkDriverUrlSecurityRule(
+                        
"file:///opt/doris/plugins/jdbc_drivers/../../etc/evil.jar"));
+        Assert.assertThrows(DdlException.class,
+                () -> 
JdbcExternalCatalog.checkDriverUrlSecurityRule("http://host/a/../b.jar";));
+        Assert.assertThrows(DdlException.class,
+                () -> JdbcExternalCatalog.checkDriverUrlSecurityRule(
+                        
"file:///opt/doris/plugins/jdbc_drivers/%2e%2e/%2e%2e/etc/evil.jar"));
+
+        
JdbcExternalCatalog.checkDriverUrlSecurityRule("mysql-connector-j-8.4.0.jar");
+        JdbcExternalCatalog.checkDriverUrlSecurityRule(
+                
"https://bucket.s3.amazonaws.com/regression/jdbc_driver/mysql-connector-j-8.4.0.jar";);
+        JdbcExternalCatalog.checkDriverUrlSecurityRule(
+                
"file:///opt/doris/plugins/jdbc_drivers/mysql-connector-j-8.4.0.jar");
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to