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]