Jdbc Deserialization in h2oai/h2o-3
Description
A vulnerability in the h2oai/h2o-3 REST API versions 3.46.0.4 allows unauthenticated remote attackers to execute arbitrary code via deserialization of untrusted data. The vulnerability exists in the endpoints POST /99/ImportSQLTable and POST /3/SaveToHiveTable, where user-controlled JDBC URLs are passed to DriverManager.getConnection, leading to deserialization if a MySQL or PostgreSQL driver is available in the classpath. This issue is fixed in version 3.47.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unauthenticated deserialization via JDBC URLs in H2O-3 REST API allows remote code execution before version 3.47.0.
Overview
CVE-2024-10553 is a critical vulnerability in the h2oai/h2o-3 REST API, specifically in endpoints POST /99/ImportSQLTable and POST /3/SaveToHiveTable. The root cause is that user-controlled JDBC URLs are passed directly to DriverManager.getConnection without validation, enabling deserialization of untrusted data when a MySQL or PostgreSQL driver is available in the classpath [1][2].
Exploitation
An unauthenticated remote attacker can supply a malicious JDBC URL containing parameters such as autoDeserialize, queryInterceptors, or allowLoadLocalInfile to trigger deserialization or other dangerous behaviors in the MySQL or PostgreSQL JDBC drivers. The attacker does not need any authentication or prior access, as these endpoints are exposed without user verification [2][4]. The classpath must include the MySQL or PostgreSQL JDBC driver for certain attack vectors, but many H2O-3 deployments include these drivers by default.
Impact
Successful exploitation allows arbitrary code execution on the server running the H2O-3 REST API. This can lead to full compromise of the machine, including data theft, service disruption, and lateral movement within the network [2][4]. The vulnerability is remotely exploitable without authentication, making it particularly dangerous for internet-facing deployments.
Mitigation
H2O-3 version 3.47.0 fixes the issue by adding JDBC URL parameter validation. The commit introduces a list of disallowed parameters (including autoDeserialize, queryInterceptors, and others) and validates the URL before connecting [3]. Users should upgrade to version 3.47.0 or later. If upgrade is not possible, network access to the vulnerable endpoints should be restricted, and the presence of MySQL/PostgreSQL JDBC drivers in the classpath should be reviewed.
- GitHub - h2oai/h2o-3: H2O is an Open Source, Distributed, Fast & Scalable Machine Learning Platform: Deep Learning, Gradient Boosting (GBM) & XGBoost, Random Forest, Generalized Linear Modeling (GLM with Elastic Net), K-Means, PCA, Generalized Additive Models (GAM), RuleFit, Support Vector Machine (SVM), Stacked Ensembles, Automatic Machine Learning (AutoML), etc.
- NVD - CVE-2024-10553
- GH-16425 Add JDBC parameter validation [nocheck] (#16432) · h2oai/h2o-3@ac1d642
- The world’s first bug bounty platform for AI/ML
AI Insight generated on May 20, 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 |
|---|---|---|
h2oPyPI | < 3.46.0.6 | 3.46.0.6 |
ai.h2o:h2o-coreMaven | < 3.46.0.6 | 3.46.0.6 |
Affected products
4- ghsa-coords2 versions
< 3.46.0.6+ 1 more
- (no CPE)range: < 3.46.0.6
- (no CPE)range: < 3.46.0.6
- h2oai/h2oai/h2o-3v5Range: unspecified
Patches
1ac1d642b4d86GH-16425 Add JDBC parameter validation [nocheck] (#16432)
2 files changed · +65 −0
h2o-core/src/main/java/water/jdbc/SQLManager.java+45 −0 modified@@ -5,10 +5,20 @@ import water.parser.ParseDataset; import water.util.Log; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; import java.sql.*; +import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class SQLManager { @@ -30,6 +40,15 @@ public class SQLManager { private static final String TMP_TABLE_ENABLED = H2O.OptArgs.SYSTEM_PROP_PREFIX + "sql.tmp_table.enabled"; + private static final String DISALLOWED_JDBC_PARAMETERS_PARAM = H2O.OptArgs.SYSTEM_PROP_PREFIX + "sql.jdbc.disallowed.parameters"; + + private static final Pattern JDBC_PARAMETERS_REGEX_PATTERN = Pattern.compile("(?i)[?;&]([a-z]+)="); + + private static final List<String> DEFAULT_JDBC_DISALLOWED_PARAMETERS = Stream.of( + "autoDeserialize", "queryInterceptors", "allowLoadLocalInfile", "allowMultiQueries", //mysql + "allowLoadLocalInfileInPath", "allowUrlInLocalInfile", "allowPublicKeyRetrieval", //mysql + "init", "script", "shutdown" //h2 + ).map(String::toLowerCase).collect(Collectors.toList()); private static AtomicLong NEXT_TABLE_NUM = new AtomicLong(0); static Key<Frame> nextTableKey(String prefix, String postfix) { @@ -58,6 +77,7 @@ public static Job<Frame> importSqlTable( final String username, final String password, final String columns, final Boolean useTempTable, final String tempTableName, final SqlFetchMode fetchMode, final Integer numChunksHint) { + validateJdbcUrl(connection_url); final Key<Frame> destination_key = nextTableKey(table, "sql_to_hex"); final Job<Frame> j = new Job<>(destination_key, Frame.class.getName(), "Import SQL Table"); @@ -533,6 +553,7 @@ private static int estimateConcurrentConnections(final int cloudSize, final shor * @throws SQLException if a database access error occurs or the url is */ public static Connection getConnectionSafe(String url, String username, String password) throws SQLException { + validateJdbcUrl(url); initializeDatabaseDriver(getDatabaseType(url)); try { return DriverManager.getConnection(url, username, password); @@ -588,6 +609,30 @@ static void initializeDatabaseDriver(String databaseType) { } } + public static void validateJdbcUrl(String jdbcUrl) throws IllegalArgumentException { + if (jdbcUrl == null || jdbcUrl.trim().isEmpty()) { + throw new IllegalArgumentException("JDBC URL is null or empty"); + } + + if (!jdbcUrl.toLowerCase().startsWith("jdbc:")) { + throw new IllegalArgumentException("JDBC URL must start with 'jdbc:'"); + } + + Matcher matcher = JDBC_PARAMETERS_REGEX_PATTERN.matcher(jdbcUrl); + String property = System.getProperty(DISALLOWED_JDBC_PARAMETERS_PARAM); + List<String> disallowedParameters = property == null ? + DEFAULT_JDBC_DISALLOWED_PARAMETERS : + Arrays.stream(property.split(",")).map(String::toLowerCase).collect(Collectors.toList()); + + while (matcher.find()) { + String key = matcher.group(1); + if (disallowedParameters.contains(key.toLowerCase())) { + throw new IllegalArgumentException("Potentially dangerous JDBC parameter found: " + key + + ". That behavior can be altered by setting " + DISALLOWED_JDBC_PARAMETERS_PARAM + " env variable to another comma separated list."); + } + } + } + static class SqlTableToH2OFrameStreaming { final String _table, _columns, _databaseType; final int _numCol;
h2o-core/src/test/java/water/jdbc/SQLManagerTest.java+20 −0 modified@@ -145,4 +145,24 @@ public void testBuildSelectChunkSql() { Assert.assertEquals("SELECT * FROM mytable LIMIT 1310 OFFSET 0", SQLManager.buildSelectChunkSql("", "mytable", 0, 1310, "*", null)); } + + @Test + public void testValidateJdbcConnectionStringH2() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Potentially dangerous JDBC parameter found: init"); + + String h2MaliciousJdbc = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS RBT AS '@groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec(\"reboot\")" + "})" + "def rbt" + "'"; + + SQLManager.validateJdbcUrl(h2MaliciousJdbc); + } + + @Test + public void testValidateJdbcConnectionStringMysql() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Potentially dangerous JDBC parameter found: autoDeserialize"); + + String mysqlMaliciousJdbc = "jdbc:mysql://domain:123/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=abcd"; + + SQLManager.validateJdbcUrl(mysqlMaliciousJdbc); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.