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

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


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

commit 678a4c070ca2bfd83890af2e2af961395bb7b3ef
Author: Calvin Kirs <[email protected]>
AuthorDate: Mon Jul 27 10:03:50 2026 +0800

    [fix](jdbc) Harden JDBC driver URL validation and remove file-upload HTTP 
API (#65987)
    
    ### What problem does this PR solve?
    
    Hardens JDBC driver loading, locks down security-sensitive configs, and
    removes an
    unnecessary file-upload attack surface on the FE.
    
    #### Summary of changes
    
    | Area | Change |
    |---|---|
    | `jdbc_driver_secure_path` matching | Component-based (structural)
    instead of a raw string prefix. `file://` URLs are decoded once via
    `URI.getPath()` and compared with `Path.startsWith`; `http(s)` URLs
    compare scheme/host/port, **user-info and query**, plus a
    component-based path prefix. Closes prefix-confusion, path-traversal
    (literal **and** percent-encoded `%2e%2e`), and query/user-info
    bypasses. `*` and empty still mean allow-all (unchanged). |
    | Fail-closed parsing | A `driver_url` that cannot be parsed as a URI is
    now rejected instead of being accepted. |
    | Mandatory CREATE/ALTER rule | New
    `JdbcDorisConnector.checkDriverUrlSecurityRule()`, invoked from the
    connector's `validateProperties()` (run by `checkProperties()` on both
    CREATE and ALTER, both `!isReplay`). It **cannot be turned off**:
    rejects any `..` segment (decoded), and requires a scheme-less
    `driver_url` to be a bare `[A-Za-z0-9._-]+.jar` name resolved under
    `jdbc_drivers_dir`. Never runs during replay or at query time. |
    | Config immutability | `jdbc_driver_url_white_list`,
    `s3_load_endpoint_white_list`, `force_sqlserver_jdbc_encrypt_false` are
    now settable only in `fe.conf` (non-mutable). |
    | Removed endpoint | `UploadAction` (`/api/{ns}/{db}/{tbl}/upload`) and
    its `LoadSubmitter` / `TmpFileMgr` helpers, plus the now-orphaned
    `http_load_submitter_max_worker_threads` config. |
    
    ### Behavior changes
    
    | # | Before | After | Impact / who is affected |
    |---|---|---|---|
    | 1 | `POST/PUT/GET/DELETE /api/{ns}/{db}/{tbl}/upload` uploaded a file
    to FE and stream-loaded it | Endpoint **removed** (404) | Web-UI "Data
    Import" page / scripts hitting this route. Stream Load, Broker Load,
    Routine Load, INSERT, S3 Load, CopyInto are **unaffected**. |
    | 2 | `driver_url` with `..`, encoded `%2e%2e`, or a scheme-less path
    with directories was accepted at CREATE (and never re-checked on ALTER)
    | Rejected at **CREATE and ALTER CATALOG** | Only new/altered catalogs.
    Existing catalogs are **not** re-validated (replay and queries
    unaffected). |
    | 3 | `jdbc_driver_secure_path` matched by raw string prefix; parse
    failures were let through | Structural matching; parse failures rejected
    (fail-closed) | Driver URLs that only passed by a coincidental prefix,
    via `..`, `%2e%2e`, or a differing query/user-info are now rejected.
    Legitimate URLs are unaffected. `*` and empty both still mean allow-all,
    so default deployments see no change. |
    | 4 | `jdbc_driver_url_white_list`, `s3_load_endpoint_white_list`,
    `force_sqlserver_jdbc_encrypt_false` were runtime-mutable | Non-mutable
    (fe.conf only, restart to take effect) | Scripts using `ADMIN SET
    FRONTEND CONFIG` for these now get a "not mutable" error. |
    | 5 | `http_load_submitter_max_worker_threads` config existed | Removed
    | Old `fe.conf` entries get the existing unknown-key warning. |
    
    ### Config changes
    
    | Config | Before | After |
    |---|---|---|
    | `jdbc_driver_url_white_list` | `mutable = true` | fe.conf only
    (non-mutable) |
    | `s3_load_endpoint_white_list` | `mutable = true` | fe.conf only
    (non-mutable) |
    | `force_sqlserver_jdbc_encrypt_false` | `mutable = true` | fe.conf only
    (non-mutable) |
    | `jdbc_driver_secure_path` | raw string-prefix match | structural
    (component-based) match; `*`/empty still allow-all |
    | `http_load_submitter_max_worker_threads` | present | removed |
    
    ### Release note
    
    Removed the `/api/{ns}/{db}/{tbl}/upload` file-upload endpoint. Hardened
    JDBC
    driver URL validation (structural `jdbc_driver_secure_path` matching
    incl.
    encoded-traversal and query/user-info handling, fail-closed parsing, and
    a
    mandatory CREATE/ALTER rule that forbids path traversal and requires a
    plain jar
    file name); `*` and empty `jdbc_driver_secure_path` still mean
    allow-all.
    `jdbc_driver_url_white_list`, `s3_load_endpoint_white_list` and
    `force_sqlserver_jdbc_encrypt_false` are now settable only in `fe.conf`.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    
    - Behavior changed:
        - [ ] No.
        - [x] Yes. <!-- See the "Behavior changes" table above. -->
    
    - Does this need documentation?
        - [ ] No.
    - [x] Yes. <!-- The removed endpoint and the config/validation changes
    should be documented. -->
---
 .../main/java/org/apache/doris/common/Config.java  |  24 +-
 .../java/org/apache/doris/common/ConfigTest.java   |  26 +-
 .../connector/jdbc/JdbcConnectorProvider.java      |   5 +
 .../doris/connector/jdbc/JdbcDorisConnector.java   |  63 ++++
 .../jdbc/JdbcConnectorProviderValidateTest.java    |  31 +-
 .../jdbc/JdbcDriverUrlSecurityRuleTest.java        |  92 ++++++
 .../org/apache/doris/catalog/JdbcResource.java     | 133 ++++++++-
 .../org/apache/doris/httpv2/rest/UploadAction.java | 318 ---------------------
 .../apache/doris/httpv2/util/LoadSubmitter.java    | 178 ------------
 .../org/apache/doris/httpv2/util/TmpFileMgr.java   | 307 --------------------
 .../org/apache/doris/catalog/JdbcResourceTest.java | 166 +++++++++++
 11 files changed, 510 insertions(+), 833 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 655af570cfb..66dc4cc881f 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
@@ -159,7 +159,9 @@ public class Config extends ConfigBase {
     @ConfField(description = {"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, it also means 
to allow all"})
+            + "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 (component-based), "
+            + "so path traversal and prefix confusion are rejected."})
     public static String jdbc_driver_secure_path = "*";
 
     @ConfField(description = {"Functions that MySQL JDBC Catalog does not 
support pushing down"})
@@ -172,7 +174,9 @@ 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 = {"Force 
SQLServer Jdbc Catalog encrypt to false"})
+    @ConfField(description = {"Force SQLServer Jdbc Catalog encrypt to false. "
+            + "This is a security-sensitive switch (it 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 = {
@@ -2846,10 +2850,6 @@ public class Config extends ConfigBase {
             "The maximum number of worker threads for the HTTP SQL 
submitter."})
     public static int http_sql_submitter_max_worker_threads = 2;
 
-    @ConfField(mutable = false, masterOnly = false, description = {
-            "The maximum number of worker threads for the HTTP upload 
submitter."})
-    public static int http_load_submitter_max_worker_threads = 2;
-
     @ConfField(mutable = true, masterOnly = true, description = {
             "The threshold of load labels' number. After this number is 
exceeded, "
                     + "the labels of the completed import jobs or tasks will 
be deleted, "
@@ -3256,9 +3256,11 @@ public class Config extends ConfigBase {
     @ConfField(mutable = true)
     public static int mow_calculate_delete_bitmap_retry_times = 10;
 
-    @ConfField(mutable = true, description = {
+    @ConfField(description = {
             "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"})
+                    + "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 = {
@@ -3290,9 +3292,11 @@ public class Config extends ConfigBase {
             ".dfs.core.cloudapi.de"
     };
 
-    @ConfField(mutable = true, description = {
+    @ConfField(description = {
             "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"})
+                    + "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 = {"The maximum length of label in Stream Load is 
limited."})
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-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcConnectorProvider.java
 
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcConnectorProvider.java
index 0f675c99d24..ac4fd613daa 100644
--- 
a/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcConnectorProvider.java
+++ 
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcConnectorProvider.java
@@ -58,6 +58,11 @@ public class JdbcConnectorProvider implements 
ConnectorProvider {
             }
         }
 
+        // 1b. Mandatory, non-configurable driver_url security rule. 
checkProperties() runs this on
+        // both CREATE and ALTER CATALOG (both !isReplay), so a malicious 
driver_url cannot be
+        // introduced by either; metadata replay of existing catalogs is never 
affected.
+        JdbcDorisConnector.checkDriverUrlSecurityRule(resolve(properties, 
JdbcConnectorProperties.DRIVER_URL));
+
         // 2. Reject deprecated lower_case_table_names
         if 
(properties.containsKey(JdbcConnectorProperties.LOWER_CASE_TABLE_NAMES)
                 || properties.containsKey(
diff --git 
a/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcDorisConnector.java
 
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcDorisConnector.java
index 750a3bdd250..d4f8464e6df 100644
--- 
a/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcDorisConnector.java
+++ 
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/JdbcDorisConnector.java
@@ -38,12 +38,15 @@ import org.apache.thrift.TSerializer;
 
 import java.io.File;
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 /**
  * JDBC connector implementation. Manages the lifecycle of
@@ -125,11 +128,71 @@ public class JdbcDorisConnector implements Connector {
         return scanPlanProvider;
     }
 
+    // A scheme-less driver_url must be a plain jar file name: letters, 
digits, dot, underscore, hyphen.
+    // This intentionally forbids any path separator, so it can never escape 
jdbc_drivers_dir.
+    private static final Pattern SAFE_DRIVER_FILE_NAME = 
Pattern.compile("^[A-Za-z0-9._-]+\\.jar$");
+
+    /**
+     * Mandatory, non-configurable driver_url security rule. It is invoked from
+     * {@link JdbcConnectorProvider#validateProperties} (and from {@link 
#preCreateValidation}),
+     * i.e. from the engine's {@code checkProperties()} hook, which runs only 
on the user-facing
+     * CREATE / ALTER CATALOG paths (both guarded by {@code !isReplay}). 
Therefore the rule never
+     * runs during metadata/edit-log replay nor at query time, so existing 
catalogs are unaffected
+     * and FE startup / follower replay can never be blocked by it.
+     *
+     * <p>The rule cannot be turned off:
+     * <ul>
+     *   <li>any {@code ..} path-traversal segment is rejected, for {@code 
file://} and {@code http(s)} alike;</li>
+     *   <li>a scheme-less driver_url must be a bare jar file name matching 
{@code [A-Za-z0-9._-]+.jar}
+     *       (no directories, no special characters), which is then resolved 
under {@code jdbc_drivers_dir}.</li>
+     * </ul>
+     * Whether a remote/absolute URL is allowed at all remains governed by the 
fe.conf-only
+     * {@code jdbc_driver_secure_path} / {@code jdbc_driver_url_white_list} 
configs; this rule only
+     * forbids traversal and enforces the bare-name charset.
+     *
+     * <p>Throws {@link IllegalArgumentException} so the engine wraps it into 
a {@code DdlException}
+     * (and, on ALTER, triggers the property rollback).
+     */
+    public static void checkDriverUrlSecurityRule(String driverUrl) {
+        if (driverUrl == null || driverUrl.isEmpty()) {
+            return;
+        }
+        // Check traversal on the decoded path so percent-encoded segments 
(e.g. %2e%2e) — which the
+        // driver-loading consumers decode — cannot slip a ".." past this rule.
+        String pathToCheck = driverUrl;
+        if (driverUrl.contains("://")) {
+            try {
+                String decoded = new URI(driverUrl).getPath();
+                if (decoded != null) {
+                    pathToCheck = decoded;
+                }
+            } catch (URISyntaxException e) {
+                throw new IllegalArgumentException("Invalid driver_url: " + 
driverUrl);
+            }
+        }
+        String probe = pathToCheck.replace('\\', '/');
+        for (String segment : probe.split("/")) {
+            if ("..".equals(segment)) {
+                throw new IllegalArgumentException(
+                        "Invalid driver_url: path traversal ('..') is not 
allowed: " + driverUrl);
+            }
+        }
+        if (!driverUrl.contains("://")) {
+            if (!SAFE_DRIVER_FILE_NAME.matcher(driverUrl).matches()) {
+                throw new IllegalArgumentException(
+                        "Invalid driver_url: a driver file name must match 
[A-Za-z0-9._-]+.jar (got: "
+                                + driverUrl + ")");
+            }
+        }
+    }
+
     @Override
     public void preCreateValidation(ConnectorValidationContext context) throws 
Exception {
         // 1. Validate/resolve JDBC driver — format, whitelist, secure_path, 
file existence.
         String driverUrl = properties.get(JdbcConnectorProperties.DRIVER_URL);
         if (driverUrl != null && !driverUrl.isEmpty()) {
+            // Mandatory, non-configurable security rule, enforced on catalog 
creation only.
+            checkDriverUrlSecurityRule(driverUrl);
             context.validateAndResolveDriverPath(driverUrl);
 
             // 2. Compute and verify checksum.
diff --git 
a/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcConnectorProviderValidateTest.java
 
b/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcConnectorProviderValidateTest.java
index 3f137720a7f..9e0261d4b80 100644
--- 
a/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcConnectorProviderValidateTest.java
+++ 
b/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcConnectorProviderValidateTest.java
@@ -33,7 +33,7 @@ public class JdbcConnectorProviderValidateTest {
     private Map<String, String> validProps() {
         Map<String, String> props = new HashMap<>();
         props.put("jdbc_url", "jdbc:mysql://localhost:3306/db");
-        props.put("driver_url", "/path/to/driver.jar");
+        props.put("driver_url", "mysql-connector-java-8.0.25.jar");
         props.put("driver_class", "com.mysql.cj.jdbc.Driver");
         return props;
     }
@@ -77,11 +77,38 @@ public class JdbcConnectorProviderValidateTest {
     public void testPrefixedPropertiesAccepted() {
         Map<String, String> props = new HashMap<>();
         props.put("jdbc.jdbc_url", "jdbc:mysql://localhost:3306/db");
-        props.put("jdbc.driver_url", "/path/to/driver.jar");
+        props.put("jdbc.driver_url", "mysql-connector-java-8.0.25.jar");
         props.put("jdbc.driver_class", "com.mysql.cj.jdbc.Driver");
         Assertions.assertDoesNotThrow(() -> 
provider.validateProperties(props));
     }
 
+    @Test
+    public void testDriverUrlTraversalRejected() {
+        // The driver_url security rule is enforced via validateProperties 
(the CREATE/ALTER hook).
+        Map<String, String> props = validProps();
+        props.put("driver_url", 
"file:///opt/doris/plugins/jdbc_drivers/../../etc/evil.jar");
+        IllegalArgumentException ex = Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> provider.validateProperties(props));
+        Assertions.assertTrue(ex.getMessage().contains("traversal"));
+    }
+
+    @Test
+    public void testDriverUrlBareNameWithDirectoryRejected() {
+        Map<String, String> props = validProps();
+        props.put("driver_url", "sub/dir/driver.jar");
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> provider.validateProperties(props));
+    }
+
+    @Test
+    public void testDriverUrlNormalHttpsAccepted() {
+        Map<String, String> props = validProps();
+        props.put("driver_url", 
"https://repo.example.com/jdbc/mysql-connector-j-8.4.0.jar";);
+        Assertions.assertDoesNotThrow(() -> 
provider.validateProperties(props));
+    }
+
     @Test
     public void testInvalidBooleanProperty() {
         Map<String, String> props = validProps();
diff --git 
a/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcDriverUrlSecurityRuleTest.java
 
b/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcDriverUrlSecurityRuleTest.java
new file mode 100644
index 00000000000..e5c75891a95
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-jdbc/src/test/java/org/apache/doris/connector/jdbc/JdbcDriverUrlSecurityRuleTest.java
@@ -0,0 +1,92 @@
+// 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.connector.jdbc;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the mandatory, non-configurable create-time driver_url security 
rule
+ * in {@link JdbcDorisConnector#checkDriverUrlSecurityRule(String)}.
+ */
+public class JdbcDriverUrlSecurityRuleTest {
+
+    // ---- rejected ----
+
+    @Test
+    public void testBareNameTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("../evil.jar"));
+    }
+
+    @Test
+    public void testBareNameWithDirectoryRejected() {
+        // A scheme-less driver_url must be a plain file name; any '/' fails 
the charset check.
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("sub/dir/driver.jar"));
+    }
+
+    @Test
+    public void testBareNameSpecialCharsRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("driver.jar; rm -rf /"));
+    }
+
+    @Test
+    public void testFileUrlTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDorisConnector.checkDriverUrlSecurityRule(
+                        
"file:///opt/doris/plugins/jdbc_drivers/../../etc/evil.jar"));
+    }
+
+    @Test
+    public void testHttpUrlTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("http://host/a/../b.jar";));
+    }
+
+    @Test
+    public void testEncodedTraversalRejected() {
+        // %2e%2e decodes to "..", which must be caught on the decoded path.
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDorisConnector.checkDriverUrlSecurityRule(
+                        
"file:///opt/doris/plugins/jdbc_drivers/%2e%2e/%2e%2e/etc/evil.jar"));
+    }
+
+    // ---- allowed ----
+
+    @Test
+    public void testPlainJarNameAllowed() {
+        Assertions.assertDoesNotThrow(
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("mysql-connector-j-8.4.0.jar"));
+        Assertions.assertDoesNotThrow(
+                () -> 
JdbcDorisConnector.checkDriverUrlSecurityRule("postgresql-42.5.0.jar"));
+    }
+
+    @Test
+    public void testNormalHttpsUrlAllowed() {
+        Assertions.assertDoesNotThrow(() -> 
JdbcDorisConnector.checkDriverUrlSecurityRule(
+                
"https://bucket.s3.amazonaws.com/regression/jdbc_driver/mysql-connector-j-8.4.0.jar";));
+    }
+
+    @Test
+    public void testNormalFileUrlAllowed() {
+        Assertions.assertDoesNotThrow(() -> 
JdbcDorisConnector.checkDriverUrlSecurityRule(
+                
"file:///opt/doris/plugins/jdbc_drivers/mysql-connector-j-8.4.0.jar"));
+    }
+}
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..0141d33d276 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 
JdbcDorisConnector.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/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 0305199d9fe..00000000000
--- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/util/LoadSubmitter.java
+++ /dev/null
@@ -1,178 +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.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", 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 334ee06db28..520a5026ad8 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;
@@ -247,4 +248,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 
in the JDBC connector).
+        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;
+        }
+    }
 }


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

Reply via email to