This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 82e1815f74 [Cherry-pick to branch-1.3] [#11839] fix(clickhouse):
escape single quotes in database and table name SQL to prevent injection
(#11840) (#11895)
82e1815f74 is described below
commit 82e1815f7486af4c2931272cae821030d1c0d83d
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Jul 6 08:59:46 2026 +0800
[Cherry-pick to branch-1.3] [#11839] fix(clickhouse): escape single quotes
in database and table name SQL to prevent injection (#11840) (#11895)
**Cherry-pick Information:**
- Original commit: c2fc58bf13b618d6169f47f6cbc24c873b4c2144
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
Co-authored-by: weijiajun <[email protected]>
---
.../operations/ClickHouseDatabaseOperations.java | 9 +++
.../operations/ClickHouseTableOperations.java | 4 +-
.../TestClickHouseDatabaseOperations.java | 18 +++++
.../TestClickHouseTableOperationsUnit.java | 87 ++++++++++++++++++++++
4 files changed, 117 insertions(+), 1 deletion(-)
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseDatabaseOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseDatabaseOperations.java
index 0d4ba9b7ea..e35946364d 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseDatabaseOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseDatabaseOperations.java
@@ -66,6 +66,15 @@ public class ClickHouseDatabaseOperations extends
JdbcDatabaseOperations {
return CLICK_HOUSE_SYSTEM_DATABASES;
}
+ @Override
+ protected String generateDatabaseExistSql(String databaseName) {
+ // Escape single quotes to prevent SQL injection.
+ // JdbcDatabaseOperations#exist executes this query via Statement (no
parameters), so we must
+ // escape values embedded in string literals.
+ String escaped = escapeSingleQuotes(databaseName);
+ return String.format("SELECT name FROM system.databases WHERE name =
'%s'", escaped);
+ }
+
@Override
public List<String> listDatabases() {
List<String> databaseNames = new ArrayList<>();
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
index e3cd9378e1..6eb48dcea0 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
@@ -111,7 +111,9 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
protected List<Index> getIndexes(Connection connection, String databaseName,
String tableName) {
// cause clickhouse not impl getPrimaryKeys yet, ref:
// https://github.com/ClickHouse/clickhouse-java/issues/1625
- String sql = QUERY_INDEXES_SQL.formatted(databaseName, tableName);
+ String sql =
+ QUERY_INDEXES_SQL.formatted(
+ escapeSingleQuotes(databaseName), escapeSingleQuotes(tableName));
try (PreparedStatement preparedStatement =
connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseDatabaseOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseDatabaseOperations.java
index e728b43736..4c39226912 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseDatabaseOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseDatabaseOperations.java
@@ -200,4 +200,22 @@ public class TestClickHouseDatabaseOperations {
Mockito.verify(connection).setCatalog("information_schema");
Mockito.verify(statement).executeUpdate("CREATE DATABASE `new_db`");
}
+
+ //
---------------------------------------------------------------------------
+ // generateDatabaseExistSql — SQL injection escape
+ //
---------------------------------------------------------------------------
+
+ @Test
+ void testGenerateDatabaseExistSqlNormalName() {
+ TestableClickHouseDatabaseOperations ops = newOps();
+ String sql = ops.generateDatabaseExistSql("my_db");
+ Assertions.assertEquals("SELECT name FROM system.databases WHERE name =
'my_db'", sql);
+ }
+
+ @Test
+ void testGenerateDatabaseExistSqlSingleQuoteEscaped() {
+ TestableClickHouseDatabaseOperations ops = newOps();
+ String sql = ops.generateDatabaseExistSql("test'db");
+ Assertions.assertEquals("SELECT name FROM system.databases WHERE name =
'test''db'", sql);
+ }
}
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
new file mode 100644
index 0000000000..e31362c782
--- /dev/null
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
@@ -0,0 +1,87 @@
+/*
+ * 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.gravitino.catalog.clickhouse.operations;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.HashMap;
+import java.util.List;
+import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseColumnDefaultValueConverter;
+import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseExceptionConverter;
+import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseTypeConverter;
+import org.apache.gravitino.rel.indexes.Index;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class TestClickHouseTableOperationsUnit {
+
+ private static final class ExposedClickHouseTableOperations extends
ClickHouseTableOperations {
+ List<Index> callGetIndexes(Connection connection, String databaseName,
String tableName)
+ throws Exception {
+ return getIndexes(connection, databaseName, tableName);
+ }
+ }
+
+ private ExposedClickHouseTableOperations newOps() {
+ ExposedClickHouseTableOperations ops = new
ExposedClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ return ops;
+ }
+
+ //
---------------------------------------------------------------------------
+ // getIndexes — SQL injection escape
+ //
---------------------------------------------------------------------------
+
+ @Test
+ void testGetIndexesSqlEscapesSingleQuotes() throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+ ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(secondaryRs.next()).thenReturn(false);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+
+ Connection connection = Mockito.mock(Connection.class);
+ ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+ Mockito.when(connection.prepareStatement(sqlCaptor.capture()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(secondaryStmt);
+
+ ops.callGetIndexes(connection, "db'1", "t'1");
+
+ // First captured SQL is the primary-key QUERY_INDEXES_SQL
(string-interpolated).
+ String primaryKeySql = sqlCaptor.getAllValues().get(0);
+ Assertions.assertTrue(
+ primaryKeySql.contains("db''1"), "database single quote should be
doubled");
+ Assertions.assertTrue(primaryKeySql.contains("t''1"), "table single quote
should be doubled");
+ }
+}