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 b578847894e branch-4.1: [fix](catalog) Unify the jdbc driver_url 
security check across jdbc/iceberg/paimon and ALTER CATALOG (#68164)
b578847894e is described below

commit b578847894e24e3fef4466d90d8d98a623107e65
Author: Calvin Kirs <[email protected]>
AuthorDate: Sat Sep 19 15:37:57 2026 +0800

    branch-4.1: [fix](catalog) Unify the jdbc driver_url security check across 
jdbc/iceberg/paimon and ALTER CATALOG (#68164)
    
    #68129
---
 .../datasource/iceberg/IcebergExternalCatalog.java |  14 +++
 .../doris/datasource/jdbc/JdbcExternalCatalog.java |  33 +-----
 .../datasource/paimon/PaimonExternalCatalog.java   |  17 ++++
 ...cebergExternalCatalogDriverUrlSecurityTest.java |  98 ++++++++++++++++++
 ...PaimonExternalCatalogDriverUrlSecurityTest.java | 112 +++++++++++++++++++++
 .../foundation/security/JdbcDriverUrlSecurity.java | 102 +++++++++++++++++++
 .../security/JdbcDriverUrlSecurityTest.java        |  94 +++++++++++++++++
 7 files changed, 442 insertions(+), 28 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalog.java
index ded5ff679dc..5a11acb7430 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalog.java
@@ -31,6 +31,7 @@ import org.apache.doris.datasource.SessionContext;
 import org.apache.doris.datasource.metacache.CacheSpec;
 import org.apache.doris.datasource.operations.ExternalMetadataOperations;
 import 
org.apache.doris.datasource.property.metastore.AbstractIcebergProperties;
+import org.apache.doris.foundation.security.JdbcDriverUrlSecurity;
 import org.apache.doris.transaction.TransactionManagerFactory;
 
 import org.apache.iceberg.catalog.Catalog;
@@ -99,6 +100,19 @@ public abstract class IcebergExternalCatalog extends 
ExternalCatalog {
                 -1L, ICEBERG_MANIFEST_CACHE_TTL_SECOND);
         
CacheSpec.checkLongProperty(catalogProperty.getOrDefault(ICEBERG_MANIFEST_CACHE_CAPACITY,
 null),
                 0L, ICEBERG_MANIFEST_CACHE_CAPACITY);
+        // Mandatory, non-configurable security rule for the driver jar the 
jdbc flavor loads into the
+        // FE JVM (shared with the jdbc / paimon-jdbc catalogs; see 
JdbcDriverUrlSecurity). Read from the
+        // raw properties and checked BEFORE the metastore-properties build 
below: that build also runs
+        // on catalog rebuild, which must never validate, and for the jdbc 
flavor it already attempts to
+        // register the driver. Key owned by IcebergJdbcMetaStoreProperties. 
Only the jdbc flavor loads
+        // a jar; on every other flavor the key is dead config that must not 
fail a catalog.
+        if 
("jdbc".equalsIgnoreCase(catalogProperty.getOrDefault(ICEBERG_CATALOG_TYPE, 
""))) {
+            try {
+                
JdbcDriverUrlSecurity.check(catalogProperty.getOrDefault("iceberg.jdbc.driver_url",
 null));
+            } catch (IllegalArgumentException e) {
+                throw new DdlException(e.getMessage(), e);
+            }
+        }
         
catalogProperty.checkMetaStoreAndStorageProperties(AbstractIcebergProperties.class);
     }
 
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 74f84577720..e363b491927 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
@@ -38,6 +38,7 @@ import 
org.apache.doris.datasource.jdbc.client.JdbcClientConfig;
 import org.apache.doris.datasource.jdbc.client.JdbcClientException;
 import org.apache.doris.datasource.mapping.IdentifierMapping;
 import org.apache.doris.datasource.mapping.JdbcIdentifierMapping;
+import org.apache.doris.foundation.security.JdbcDriverUrlSecurity;
 import org.apache.doris.proto.InternalService;
 import org.apache.doris.proto.InternalService.PJdbcTestConnectionRequest;
 import org.apache.doris.proto.InternalService.PJdbcTestConnectionResult;
@@ -60,17 +61,13 @@ 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,
@@ -128,30 +125,10 @@ public class JdbcExternalCatalog extends ExternalCatalog {
      * 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 + ")");
+        try {
+            JdbcDriverUrlSecurity.check(driverUrl);
+        } catch (IllegalArgumentException e) {
+            throw new DdlException(e.getMessage(), e);
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalCatalog.java
index ee4b7962bf3..99dbd09c1cf 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalCatalog.java
@@ -28,6 +28,7 @@ import org.apache.doris.datasource.metacache.CacheSpec;
 import org.apache.doris.datasource.metacache.ExternalMetaCacheBudgetManager;
 import org.apache.doris.datasource.operations.ExternalMetadataOperations;
 import org.apache.doris.datasource.property.metastore.AbstractPaimonProperties;
+import org.apache.doris.foundation.security.JdbcDriverUrlSecurity;
 import org.apache.doris.transaction.TransactionManagerFactory;
 
 import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -232,6 +233,22 @@ public class PaimonExternalCatalog extends ExternalCatalog 
{
         // Validate only newly supplied dynamic options on ALTER. This lets an 
old image containing
         // a formerly accepted option survive an unrelated update while still 
rejecting new writes.
         
PaimonReaderOptions.validateCatalogProperties(strictlyValidatedProperties);
+        // Mandatory, non-configurable security rule for the driver jar the 
jdbc flavor loads into the
+        // FE JVM (shared with the jdbc / iceberg-jdbc catalogs; see 
JdbcDriverUrlSecurity). Both this
+        // catalog's CREATE hook (checkProperties()) and its detached ALTER 
hook
+        // (validatePropertiesBeforeUpdate) funnel through here, and never the 
replay/rebuild path.
+        // Checked BEFORE the metastore-properties build below, which for the 
jdbc flavor already
+        // attempts to register the driver. Keys owned by 
PaimonJdbcMetaStoreProperties; only the jdbc
+        // flavor loads a jar, on every other flavor they are dead config that 
must not fail a catalog.
+        if ("jdbc".equalsIgnoreCase(property.getOrDefault(PAIMON_CATALOG_TYPE, 
""))) {
+            for (String key : new String[] {"paimon.jdbc.driver_url", 
"jdbc.driver_url"}) {
+                try {
+                    JdbcDriverUrlSecurity.check(property.getOrDefault(key, 
null));
+                } catch (IllegalArgumentException e) {
+                    throw new DdlException(e.getMessage(), e);
+                }
+            }
+        }
         
property.checkMetaStoreAndStorageProperties(AbstractPaimonProperties.class);
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalogDriverUrlSecurityTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalogDriverUrlSecurityTest.java
new file mode 100644
index 00000000000..8fdb7a509e5
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergExternalCatalogDriverUrlSecurityTest.java
@@ -0,0 +1,98 @@
+// 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.datasource.iceberg;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.datasource.CatalogProperty;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The Iceberg JDBC catalog loads {@code iceberg.jdbc.driver_url} into the FE 
JVM, exactly like the jdbc
+ * catalog does. These tests pin that {@code checkProperties()} — the 
statement-time hook the engine runs
+ * on CREATE and on ALTER, and never on replay or catalog rebuild — reaches 
the SAME mandatory rule
+ * ({@code JdbcDriverUrlSecurity}). The rule's own semantics are pinned once in
+ * {@code JdbcDriverUrlSecurityTest} (fe-foundation).
+ */
+public class IcebergExternalCatalogDriverUrlSecurityTest {
+
+    @Before
+    public void setUp() {
+        FeConstants.runningUnitTest = true;
+    }
+
+    private static IcebergExternalCatalog catalogWith(Map<String, String> 
props) {
+        // The base constructor takes no properties; an anonymous subclass may 
set the protected field.
+        return new IcebergExternalCatalog(1L, "iceberg_driver_url_test", "") {
+            {
+                catalogProperty = new CatalogProperty(null, props);
+            }
+        };
+    }
+
+    private static Map<String, String> jdbcProps(String driverUrl) {
+        Map<String, String> props = new HashMap<>();
+        props.put("type", "iceberg");
+        props.put("iceberg.catalog.type", "jdbc");
+        props.put("uri", "jdbc:mysql://127.0.0.1:3306/iceberg");
+        props.put("warehouse", "s3://bucket/wh");
+        props.put("iceberg.jdbc.driver_url", driverUrl);
+        props.put("iceberg.jdbc.driver_class", "com.mysql.cj.jdbc.Driver");
+        return props;
+    }
+
+    @Test
+    public void checkPropertiesRejectsTraversalDriverUrl() {
+        // MUTATION: drop the JdbcDriverUrlSecurity.check call from 
IcebergExternalCatalog.checkProperties
+        // -> the traversal URL survives to the metastore-properties build and 
driver registration -> red.
+        DdlException e = Assert.assertThrows(DdlException.class,
+                () -> 
catalogWith(jdbcProps("file:///opt/drivers/../../etc/evil.jar")).checkProperties());
+        Assert.assertTrue(e.getMessage(), e.getMessage().contains("path 
traversal"));
+    }
+
+    @Test
+    public void checkPropertiesRejectsSchemelessPathDriverUrl() {
+        DdlException e = Assert.assertThrows(DdlException.class,
+                () -> 
catalogWith(jdbcProps("sub/dir/evil.jar")).checkProperties());
+        Assert.assertTrue(e.getMessage(), e.getMessage().contains("must 
match"));
+    }
+
+    @Test
+    public void nonJdbcFlavorSkipsTheRule() {
+        // On a REST catalog the key is dead config that never reaches a class 
loader; the rule must not
+        // turn such a catalog into a CREATE/ALTER failure. The tail of 
checkProperties may fail for
+        // unrelated REST reasons in this bare unit-test environment, so only 
the rule's absence is pinned.
+        Map<String, String> props = new HashMap<>();
+        props.put("type", "iceberg");
+        props.put("iceberg.catalog.type", "rest");
+        props.put("uri", "http://127.0.0.1:8181";);
+        props.put("iceberg.jdbc.driver_url", "../evil.jar");
+        try {
+            catalogWith(props).checkProperties();
+        } catch (Exception e) {
+            Assert.assertFalse(e.getMessage(),
+                    e.getMessage() != null && e.getMessage().contains("path 
traversal"));
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonExternalCatalogDriverUrlSecurityTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonExternalCatalogDriverUrlSecurityTest.java
new file mode 100644
index 00000000000..d18781db9e2
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonExternalCatalogDriverUrlSecurityTest.java
@@ -0,0 +1,112 @@
+// 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.datasource.paimon;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.FeConstants;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The Paimon JDBC catalog loads {@code paimon.jdbc.driver_url} / {@code 
jdbc.driver_url} into the FE JVM,
+ * exactly like the jdbc catalog does. These tests pin that BOTH 
statement-time hooks — the CREATE-side
+ * {@code checkProperties()} and the detached ALTER-side {@code 
validatePropertiesBeforeUpdate} — reach the
+ * SAME mandatory rule ({@code JdbcDriverUrlSecurity}) through this catalog's 
shared private validation.
+ * Neither hook runs on replay or catalog rebuild. The rule's own semantics 
are pinned once in
+ * {@code JdbcDriverUrlSecurityTest} (fe-foundation).
+ */
+public class PaimonExternalCatalogDriverUrlSecurityTest {
+
+    @Before
+    public void setUp() {
+        FeConstants.runningUnitTest = true;
+    }
+
+    private static Map<String, String> jdbcProps(String driverUrlKey, String 
driverUrl) {
+        Map<String, String> props = new HashMap<>();
+        props.put("type", "paimon");
+        props.put("paimon.catalog.type", "jdbc");
+        props.put("uri", "jdbc:mysql://127.0.0.1:3306/paimon");
+        props.put("warehouse", "s3://bucket/wh");
+        props.put(driverUrlKey, driverUrl);
+        props.put("jdbc.driver_class", "com.mysql.cj.jdbc.Driver");
+        return props;
+    }
+
+    private static PaimonExternalCatalog catalogWith(Map<String, String> 
props) {
+        return new PaimonExternalCatalog(1L, "paimon_driver_url_test", null, 
props, "");
+    }
+
+    @Test
+    public void createRejectsTraversalDriverUrl() {
+        // MUTATION: drop the JdbcDriverUrlSecurity.check loop from 
PaimonExternalCatalog's private
+        // checkProperties -> the traversal URL survives to the metastore 
build and registration -> red.
+        DdlException e = Assert.assertThrows(DdlException.class,
+                () -> catalogWith(jdbcProps("jdbc.driver_url", 
"file:///opt/drivers/../../etc/evil.jar"))
+                        .checkProperties());
+        Assert.assertTrue(e.getMessage(), e.getMessage().contains("path 
traversal"));
+    }
+
+    @Test
+    public void createRejectsTraversalOnPaimonPrefixedAlias() {
+        DdlException e = Assert.assertThrows(DdlException.class,
+                () -> catalogWith(
+                        jdbcProps("paimon.jdbc.driver_url", 
"file:///opt/drivers/../../etc/evil.jar"))
+                        .checkProperties());
+        Assert.assertTrue(e.getMessage(), e.getMessage().contains("path 
traversal"));
+    }
+
+    @Test
+    public void alterRejectsRepointedDriverUrl() {
+        // The detached ALTER hook funnels through the same private validation 
as CREATE; a repointed
+        // driver_url in the merged candidate must be rejected before anything 
is published.
+        PaimonExternalCatalog catalog = catalogWith(
+                jdbcProps("jdbc.driver_url", "mysql-connector-j-8.4.0.jar"));
+        Map<String, String> update = new HashMap<>();
+        update.put("jdbc.driver_url", 
"file:///opt/drivers/../../etc/evil.jar");
+
+        DdlException e = Assert.assertThrows(DdlException.class,
+                () -> catalog.validatePropertiesBeforeUpdate(
+                        jdbcProps("jdbc.driver_url", 
"mysql-connector-j-8.4.0.jar"), update));
+        Assert.assertTrue(e.getMessage(), e.getMessage().contains("path 
traversal"));
+    }
+
+    @Test
+    public void nonJdbcFlavorSkipsTheRule() {
+        // On a filesystem catalog the keys are dead config that never reach a 
class loader; the rule must
+        // not turn such a catalog into a CREATE/ALTER failure. The tail of 
the validation may fail for
+        // unrelated storage reasons in this bare unit-test environment, so 
only the rule's absence is
+        // pinned.
+        Map<String, String> props = new HashMap<>();
+        props.put("type", "paimon");
+        props.put("paimon.catalog.type", "filesystem");
+        props.put("warehouse", "s3://bucket/wh");
+        props.put("jdbc.driver_url", "../evil.jar");
+        try {
+            catalogWith(props).checkProperties();
+        } catch (Exception e) {
+            Assert.assertFalse(e.getMessage(),
+                    e.getMessage() != null && e.getMessage().contains("path 
traversal"));
+        }
+    }
+}
diff --git 
a/fe/fe-foundation/src/main/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurity.java
 
b/fe/fe-foundation/src/main/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurity.java
new file mode 100644
index 00000000000..fae1b921673
--- /dev/null
+++ 
b/fe/fe-foundation/src/main/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurity.java
@@ -0,0 +1,102 @@
+// 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.foundation.security;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.regex.Pattern;
+
+/**
+ * The mandatory, non-configurable {@code driver_url} security rule, shared by 
every connector that
+ * loads a JDBC driver jar into the FE JVM.
+ *
+ * <p>Three catalog types reach the same {@code URLClassLoader} + {@code 
Class.forName(name, true, loader)}
+ * sink from a user-supplied catalog property, so they must share one rule 
rather than each re-deriving it:
+ * the {@code jdbc} catalog ({@code driver_url}), the Iceberg JDBC catalog
+ * ({@code iceberg.jdbc.driver_url}) and the Paimon JDBC catalog
+ * ({@code paimon.jdbc.driver_url} / {@code jdbc.driver_url}). This class is 
that single source of truth;
+ * it lives in fe-foundation because that is the one module every properties 
holder already depends on.
+ *
+ * <p>The rule cannot be turned off:
+ * <ul>
+ *   <li>any {@code ..} path-traversal segment is rejected, for {@code 
file://} and {@code http(s)} alike,
+ *       checked on the percent-decoded path so {@code %2e%2e} cannot slip 
past;</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 
the connector's drivers
+ *       directory.</li>
+ * </ul>
+ * Whether a remote/absolute URL is allowed <em>at all</em> remains governed 
by the fe.conf-only
+ * {@code jdbc_driver_secure_path} / {@code jdbc_driver_url_white_list} 
configs, which the engine applies
+ * separately; this rule only forbids traversal and enforces the bare-name 
charset.
+ *
+ * <p><b>Where callers invoke it: statement-time validation only.</b> The call 
sites are the
+ * catalogs' {@code checkProperties()} hooks ({@code JdbcExternalCatalog},
+ * {@code IcebergExternalCatalog}, {@code PaimonExternalCatalog}), which the 
engine reaches from the
+ * user-facing CREATE and ALTER CATALOG paths and never from edit-log replay 
or a catalog rebuild.
+ * That placement is load-bearing: a catalog created before this rule existed 
must keep coming back
+ * after an FE restart, so the rule must never run from a metastore-properties 
build.
+ *
+ * <p>Throws {@link IllegalArgumentException} so the engine wraps it into a 
{@code DdlException}
+ * (and, on ALTER, triggers the property rollback).
+ */
+public final class JdbcDriverUrlSecurity {
+
+    // 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 
the drivers directory.
+    private static final Pattern SAFE_DRIVER_FILE_NAME = 
Pattern.compile("^[A-Za-z0-9._-]+\\.jar$");
+
+    private JdbcDriverUrlSecurity() {
+    }
+
+    /**
+     * Applies the rule to a raw, alias-resolved {@code driver_url}. A 
null/empty value means "use the
+     * engine-provided driver" and is accepted; every other value must satisfy 
the rule above.
+     */
+    public static void check(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 + ")");
+            }
+        }
+    }
+}
diff --git 
a/fe/fe-foundation/src/test/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurityTest.java
 
b/fe/fe-foundation/src/test/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurityTest.java
new file mode 100644
index 00000000000..e3f58466dcf
--- /dev/null
+++ 
b/fe/fe-foundation/src/test/java/org/apache/doris/foundation/security/JdbcDriverUrlSecurityTest.java
@@ -0,0 +1,94 @@
+// 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.foundation.security;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the mandatory, non-configurable driver_url security rule in
+ * {@link JdbcDriverUrlSecurity#check(String)}, shared by the jdbc, 
iceberg-jdbc and paimon-jdbc catalogs.
+ * The per-connector tests assert only that each catalog type reaches this 
rule; the rule's own semantics
+ * are pinned here, once.
+ */
+public class JdbcDriverUrlSecurityTest {
+
+    // ---- rejected ----
+
+    @Test
+    public void testBareNameTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDriverUrlSecurity.check("../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,
+                () -> JdbcDriverUrlSecurity.check("sub/dir/driver.jar"));
+    }
+
+    @Test
+    public void testBareNameSpecialCharsRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDriverUrlSecurity.check("driver.jar; rm -rf /"));
+    }
+
+    @Test
+    public void testFileUrlTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDriverUrlSecurity.check(
+                        
"file:///opt/doris/plugins/jdbc_drivers/../../etc/evil.jar"));
+    }
+
+    @Test
+    public void testHttpUrlTraversalRejected() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> JdbcDriverUrlSecurity.check("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,
+                () -> JdbcDriverUrlSecurity.check(
+                        
"file:///opt/doris/plugins/jdbc_drivers/%2e%2e/%2e%2e/etc/evil.jar"));
+    }
+
+    // ---- accepted ----
+
+    @Test
+    public void testPlainJarNameAllowed() {
+        Assertions.assertDoesNotThrow(
+                () -> 
JdbcDriverUrlSecurity.check("mysql-connector-j-8.4.0.jar"));
+        Assertions.assertDoesNotThrow(
+                () -> JdbcDriverUrlSecurity.check("postgresql-42.5.0.jar"));
+    }
+
+    @Test
+    public void testNormalHttpsUrlAllowed() {
+        Assertions.assertDoesNotThrow(() -> JdbcDriverUrlSecurity.check(
+                
"https://bucket.s3.amazonaws.com/regression/jdbc_driver/mysql-connector-j-8.4.0.jar";));
+    }
+
+    @Test
+    public void testNormalFileUrlAllowed() {
+        Assertions.assertDoesNotThrow(() -> JdbcDriverUrlSecurity.check(
+                
"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