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");
+  }
+}

Reply via email to