Apache Hive: SQL injection vulnerability when processing delete column statistics requests via the HMS Thrift APIs
Description
SQL injection vulnerability in Hive Metastore Server (HMS) when processing delete column statistics requests via the Thrift APIs. The vulnerability is only exploitable by trusted/authorized users/applications that are allowed to call directly the Thrift APIs. In most real-world deployments, HMS is accessible to only a handful of applications (e.g., Hiveserver2) thus the vulnerability is not exploitable. Moreover, the vulnerable code cannot be reached when metastore.try.direct.sql property is set to false.
This issue affects Apache Hive: from 4.1.0 before 4.2.0.
Users are recommended to upgrade to version 4.2.0, which fixes the issue. Users who cannot upgrade directly are encouraged to set metastore.try.direct.sql property to false if the HMS Thrift APIs are exposed to general public.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Hive 4.1.0 to 4.2.0 contains a SQL injection in HMS delete column statistics via Thrift, exploitable only by authorized users; fixed in 4.2.0.
Vulnerability
Overview
CVE-2025-62728 is a SQL injection vulnerability in Apache Hive's Metastore's Hive Metastore Server (HMS) when processing delete column statistics requests via the Thrift APIs [1]. The root cause is the lack of input validation on the engine parameter extracted from DeleteColumnStatisticsRequest; this unvalidated value is passed directly into SQL statement construction in MetaStoreDirectSql.deleteTableColumnStatistics [3]. The vulnerable code path exists in HMS versions 4.1.0 up to but not including 4.2.0 [1].
Exploitation
Context
Exploitation requires the attacker to be a trusted or authorized user or application that can directly call the HMS Thrift APIs [1]. In typical deployments, HMS is only accessible to a limited set of applications (e.g., HiveServer2), which significantly reduces the attack surface [1]. Additionally, the vulnerable code cannot be reached when the metastore.try.direct.sql property is set to false [1]. The commit that fixes the issue includes proper escaping of column identifiers and the engine parameter to prevent SQL injection [2].
Impact
A successful SQL injection could allow an attacker to execute arbitrary SQL commands on the metastore database, potentially leading to unauthorized data access or manipulation [3]. However, due to the restricted access to the Thrift API and the availability of a configuration workaround, the practical risk is limited to be considered limited in most environments [1].
Mitigation
Users are strongly recommended to upgrade to Apache Hive 4.2.0, which contains the fix [1]. For those who cannot upgrade immediately, setting metastore.try.direct.sql to false will prevent the vulnerable code from being executed, provided that the HMS Thrift APIs are exposed to general public [1]. No other workarounds have been published.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.hive:hive-commonMaven | >= 4.1.0, < 4.2.0 | 4.2.0 |
org.apache.hive:hive-metastoreMaven | >= 4.1.0, < 4.2.0 | 4.2.0 |
Affected products
2- Apache Software Foundation/Apache Hivev5Range: 4.1.0
Patches
1c18d0df27021Enable using column identifiers with special characters when deleting table column statistics. (#6149)
2 files changed · +57 −21
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/MetaStoreDirectSql.java+30 −14 modified@@ -133,6 +133,7 @@ */ class MetaStoreDirectSql { private static final int NO_BATCHING = -1, DETECT_BATCHING = 0; + private static final Set<String> ALLOWED_TABLES_TO_LOCK = Set.of("NOTIFICATION_SEQUENCE"); private static final Logger LOG = LoggerFactory.getLogger(MetaStoreDirectSql.class); private final PersistenceManager pm; @@ -3203,6 +3204,11 @@ private void getStatsTableListResult( } public void lockDbTable(String tableName) throws MetaException { + // Only certain tables are allowed to be locked, and the API should restrict them. + if (!ALLOWED_TABLES_TO_LOCK.contains(tableName)) { + throw new MetaException("Error while locking table " + tableName); + } + String lockCommand = "lock table \"" + tableName + "\" in exclusive mode"; try { executeNoResult(lockCommand); @@ -3243,19 +3249,26 @@ public void deleteColumnStatsState(long tbl_id) throws MetaException { } public boolean deleteTableColumnStatistics(long tableId, List<String> colNames, String engine) { - String deleteSql = "delete from " + TAB_COL_STATS + " where \"TBL_ID\" = " + tableId; + String deleteSql = "delete from " + TAB_COL_STATS + " where \"TBL_ID\" = ?"; + List<Object> params = new ArrayList<>(colNames == null ? 2 : colNames.size() + 2); + params.add(tableId); + if (colNames != null && !colNames.isEmpty()) { - deleteSql += " and \"COLUMN_NAME\" in (" + colNames.stream().map(col -> "'" + col + "'").collect(Collectors.joining(",")) + ")"; + deleteSql += " and \"COLUMN_NAME\" in (" + makeParams(colNames.size()) + ")"; + params.addAll(colNames); } + if (engine != null) { - deleteSql += " and \"ENGINE\" = '" + engine + "'"; + deleteSql += " and \"ENGINE\" = ?"; + params.add(engine); } - try { - executeNoResult(deleteSql); - } catch (SQLException e) { - LOG.warn("Error removing table column stats. ", e); + + try (QueryWrapper queryParams = new QueryWrapper(pm.newQuery("javax.jdo.query.SQL", deleteSql))) { + executeWithArray(queryParams.getInnerQuery(), params.toArray(), deleteSql); + } catch (MetaException e) { return false; } + return true; } @@ -3269,17 +3282,20 @@ public List<Void> run(List<String> input) throws Exception { input, Collections.emptyList(), -1); if (!partitionIds.isEmpty()) { String deleteSql = "delete from " + PART_COL_STATS + " where \"PART_ID\" in ( " + getIdListForIn(partitionIds) + ")"; + List<Object> params = new ArrayList<>(colNames == null ? 1 : colNames.size() + 1); + if (colNames != null && !colNames.isEmpty()) { - deleteSql += " and \"COLUMN_NAME\" in (" + colNames.stream().map(col -> "'" + col + "'").collect(Collectors.joining(",")) + ")"; + deleteSql += " and \"COLUMN_NAME\" in (" + makeParams(colNames.size()) + ")"; + params.addAll(colNames); } + if (engine != null) { - deleteSql += " and \"ENGINE\" = '" + engine + "'"; + deleteSql += " and \"ENGINE\" = ?"; + params.add(engine); } - try { - executeNoResult(deleteSql); - } catch (SQLException e) { - LOG.warn("Error removing partition column stats. ", e); - throw new MetaException("Error removing partition column stats: " + e.getMessage()); + + try (QueryWrapper queryParams = new QueryWrapper(pm.newQuery("javax.jdo.query.SQL", deleteSql))) { + executeWithArray(queryParams.getInnerQuery(), params.toArray(), deleteSql); } } return null;
standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestObjectStore.java+27 −7 modified@@ -919,42 +919,48 @@ public void testTableStatisticsOps() throws Exception { List<ColumnStatistics> tabColStats; try (AutoCloseable c = deadline()) { tabColStats = objectStore.getTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, - Arrays.asList("test_col1", "test_col2")); + Arrays.asList("test_col1", "test_col' 2")); } Assert.assertEquals(0, tabColStats.size()); ColumnStatisticsDesc statsDesc = new ColumnStatisticsDesc(true, DB1, TABLE1); ColumnStatisticsObj statsObj1 = new ColumnStatisticsObj("test_col1", "int", new ColumnStatisticsData(ColumnStatisticsData._Fields.DECIMAL_STATS, new DecimalColumnStatsData(100, 1000))); - ColumnStatisticsObj statsObj2 = new ColumnStatisticsObj("test_col2", "int", + ColumnStatisticsObj statsObj2 = new ColumnStatisticsObj("test_col' 2", "int", new ColumnStatisticsData(ColumnStatisticsData._Fields.DECIMAL_STATS, new DecimalColumnStatsData(200, 2000))); ColumnStatistics colStats = new ColumnStatistics(statsDesc, Arrays.asList(statsObj1, statsObj2)); colStats.setEngine(ENGINE); objectStore.updateTableColumnStatistics(colStats, null, 0); try (AutoCloseable c = deadline()) { tabColStats = objectStore.getTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, - Arrays.asList("test_col1", "test_col2")); + Arrays.asList("test_col1", "test_col' 2")); } Assert.assertEquals(1, tabColStats.size()); Assert.assertEquals(2, tabColStats.get(0).getStatsObjSize()); objectStore.deleteTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, "test_col1", ENGINE); try (AutoCloseable c = deadline()) { tabColStats = objectStore.getTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, - Arrays.asList("test_col1", "test_col2")); + Arrays.asList("test_col1", "test_col' 2")); } Assert.assertEquals(1, tabColStats.size()); Assert.assertEquals(1, tabColStats.get(0).getStatsObjSize()); - objectStore.deleteTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, "test_col2", ENGINE); + objectStore.deleteTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, "test_col' 2", ENGINE); try (AutoCloseable c = deadline()) { tabColStats = objectStore.getTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, - Arrays.asList("test_col1", "test_col2")); + Arrays.asList("test_col1", "test_col' 2")); } Assert.assertEquals(0, tabColStats.size()); } + @Test + public void testDeleteTableColumnStatisticsWhenEngineHasSpecialCharacter() throws Exception { + createPartitionedTable(true, true); + objectStore.deleteTableColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, "test_col1", "special '"); + } + @Test public void testPartitionStatisticsOps() throws Exception { createPartitionedTable(true, true); @@ -1006,6 +1012,14 @@ public void testPartitionStatisticsOps() throws Exception { Assert.assertEquals(0, stat.size()); } + @Test + public void testDeletePartitionColumnStatisticsWhenEngineHasSpecialCharacter() throws Exception { + createPartitionedTable(true, true); + objectStore.deletePartitionColumnStatistics(DEFAULT_CATALOG_NAME, DB1, TABLE1, + "test_part_col=a2", List.of("a2"), null, "special '"); + } + + @Test public void testAggrStatsUseDB() throws Exception { Configuration conf2 = MetastoreConf.newMetastoreConf(conf); @@ -1051,7 +1065,7 @@ private void createPartitionedTable(boolean withPrivileges, boolean withStatisti .setDbName(DB1) .setTableName(TABLE1) .addCol("test_col1", "int") - .addCol("test_col2", "int") + .addCol("test_col' 2", "int") .addPartCol("test_part_col", "int") .addCol("test_bucket_col", "int", "test bucket col comment") .addCol("test_skewed_col", "int", "test skewed col comment") @@ -1239,6 +1253,12 @@ protected Database getJdoResult(ObjectStore.GetHelper<Database> ctx) throws Meta Assert.assertEquals(1, directSqlErrors.getCount()); } + @Test(expected = MetaException.class) + public void testLockDbTableThrowsExceptionWhenTableIsNotAllowedToLock() throws Exception { + MetaStoreDirectSql metaStoreDirectSql = new MetaStoreDirectSql(objectStore.getPersistenceManager(), conf, null); + metaStoreDirectSql.lockDbTable("TBLS"); + } + @Deprecated private static void dropAllStoreObjects(RawStore store) throws MetaException, InvalidObjectException, InvalidInputException {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-932v-x9x2-vq29ghsaADVISORY
- lists.apache.org/thread/yj65dd8dmzgy8p3nv8zy33v8knzg9o7gghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-62728ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/11/26/3ghsaWEB
- github.com/apache/hive/commit/c18d0df2702130cf5d0f050e516eb8999aa56301ghsaWEB
- issues.apache.org/jira/browse/HIVE-29269ghsaWEB
News mentions
0No linked articles in our index yet.