# Root Cause Analysis — CVE-2026-41042

## Summary

Apache Gravitino (before 1.2.1) exposes an unauthenticated REST endpoint
`POST /api/metalakes/{metalake}/catalogs/testConnection` that lets a caller
supply an arbitrary JDBC URL for a catalog connection test. Because no
validation rejected H2 JDBC URLs or the H2 driver class, an attacker could
supply a malicious `jdbc:h2:...;INIT=...` URL. H2's `INIT` connection
parameter runs arbitrary SQL when the database is first opened, and H2's
`CREATE ALIAS ... AS '<java source>'` compiles and executes arbitrary Java
code inside the Gravitino server JVM. By combining `CREATE ALIAS` with
`Runtime`/`ProcessBuilder` calls, an unauthenticated remote caller achieves
arbitrary operating-system command execution on the server.

## Impact

- **Package/component affected:** `org.apache.gravitino:gravitino-catalog-jdbc-common`
  (`DataSourceUtils.createDataSource`), reached through the
  `testConnection` REST endpoint and the JDBC catalog operations
  (`JdbcCatalogOperations.initialize`).
- **Affected versions:** Apache Gravitino before 1.2.1 (confirmed on the
  official `1.2.0` binary distribution; H2 `1.4.200` is bundled).
- **Risk level / consequences:** Remote code execution. The default server
  configuration uses the `SimpleAuthenticator` (accepts anonymous requests)
  and has `gravitino.authorization.enable = false`, so the endpoint is
  reachable without any credentials. The H2 driver is always on the server
  classpath (it is the default entity-store backend) and is treated as a
  "shared" class by the catalog `IsolatedClassLoader`, so attacker-supplied
  H2 URLs resolve to the bundled driver even for non-H2 catalog providers.

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote code execution —
  "executes arbitrary Java code on the server via H2's INIT parameter",
  expected impact `code_execution`.
- **Reproduced impact from this run:** Arbitrary OS command execution
  confirmed end-to-end through the unauthenticated REST API. The malicious
  `testConnection` call returned HTTP 200 / `{"code":0}` and a marker file
  containing the output of the `id` command
  (`uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)`) was written by
  a `ProcessBuilder` started from H2-compiled Java code running in the
  server JVM.
- **Parity:** `full`.
- **Not demonstrated:** N/A — the full code-execution chain was demonstrated.

## Root Cause

`DataSourceUtils.createDataSource(JdbcConfig)` is the single entry point for
all user-facing catalog JDBC connections. In the vulnerable version it
performed no allow-listing of JDBC drivers/URLs: it passed the caller-
controlled `jdbc-url` and `jdbc-driver` straight into a DBCP
`BasicDataSource`. The shared validator `JdbcUrlUtils.validateJdbcConfig`
only inspects MySQL/MariaDB/PostgreSQL URLs for unsafe parameters and
intentionally does **not** touch H2 (H2 is the legitimate embedded
entity-store backend). Consequently an H2 URL reached the driver untouched.

The call chain triggered by `testConnection` is:

```
CatalogOperations.testConnection (REST, @Path "testConnection")
  -> CatalogManager.testConnection
    -> CatalogWrapper.doWithCatalogOps
      -> BaseCatalog.ops()  (lazy initialize)
        -> JdbcCatalogOperations.initialize(conf, ...)
          -> DataSourceUtils.createDataSource(jdbcConfig)   <-- vulnerable
            -> BasicDataSource (url=jdbc:h2:mem:...;INIT=..., driver=org.h2.Driver)
```

When the pool opens its first connection (during
`JdbcCatalogOperations.testConnection -> databaseOperation.listDatabases()`),
H2 processes the `INIT` parameter. The payload
`INIT=CREATE ALIAS EXEC AS 'String f() throws Exception { new ProcessBuilder(...).start().waitFor(); ... }'\;CALL EXEC()`
compiles the Java source via the JDK compiler and invokes it, executing the
attacker's OS command inside the server process.

**Fix commit:** `5daabcd0e8ddc96e25bd6c6ce7b153cdb311c3f2`
("[MINOR] fix(catalog): block H2 JDBC URL and driver in catalog datasource
creation (#10801)"), cherry-picked to the 1.2 release branch as
`84d3de9c7` and shipped in Gravitino 1.2.1. The fix adds a case-insensitive
block in `DataSourceUtils.createDataSource()` that rejects any URL whose
decoded form starts with `jdbc:h2` and any driver class starting with
`org.h2.`, throwing `GravitinoRuntimeException` before the datasource is
created.

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained,
   idempotent, run twice consecutively — both runs pass with exit 0).
2. **What it does:**
   - Installs OpenJDK 17 if needed.
   - Downloads/reuses the official Apache Gravitino binary distributions
     `gravitino-1.2.0-bin.tar.gz` (vulnerable) and
     `gravitino-1.2.1-bin.tar.gz` (fixed) from the GitHub release CDN
     (fallback: Apache archive).
   - For each version: extracts a clean copy, disables auxiliary services,
     starts the real `gravitino.sh` server, waits for the `/api/version`
     healthcheck, creates a metalake, and sends an **unauthenticated**
     `POST /api/metalakes/test_ml/catalogs/testConnection` with
     `provider=jdbc-postgresql`, `jdbc-driver=org.h2.Driver`, and a
     malicious `jdbc-url` whose `INIT` runs `CREATE ALIAS` + `ProcessBuilder`
     to execute `id` and write its output to a marker file. Two attempts are
     made per version (each attempt uses a fresh H2 in-memory DB name because
     H2 only runs `INIT` when a database is first created).
   - Captures per-attempt request/response logs, server logs, version JSON,
     and the RCE marker file under `bundle/logs/`.
   - Writes `bundle/repro/runtime_manifest.json`.
3. **Expected evidence:**
   - Vulnerable 1.2.0: `testConnection` returns HTTP 200 `{"code":0}` and the
     marker file is created containing `id` output → RCE.
   - Fixed 1.2.1: `testConnection` returns HTTP 500
     `"H2 JDBC URL is not allowed in catalog configuration"` (thrown at
     `DataSourceUtils.createDataSource:54`) and no marker file is created.

## Evidence

- `bundle/logs/reproduction_steps.log` — orchestrator log with verdict.
- `bundle/logs/vuln_version.json` — `1.2.0`, gitCommit `1c1665f0...`.
- `bundle/logs/fixed_version.json` — `1.2.1`, gitCommit `e88bffcfc...`.
- `bundle/logs/vuln_attempt_1.log` / `vuln_attempt_2.log` —
  `testConnection_status: 200`, `testConnection_body: {"code":0}`,
  `marker_exists: True`, `marker_content: uid=1000(vscode) ...`.
- `bundle/logs/rce_marker_vuln_latest.txt` — the file written by the
  attacker's OS command inside the server JVM.
- `bundle/logs/fixed_attempt_1.log` / `fixed_attempt_2.log` —
  `testConnection_status: 500`, message
  `"H2 JDBC URL is not allowed in catalog configuration"`, stack at
  `DataSourceUtils.createDataSource(DataSourceUtils.java:54)`,
  `marker_exists: False`.
- `bundle/logs/vuln_server.log` / `fixed_server.log` — server-side logs.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest.

Key excerpts:

```
# Vulnerable 1.2.0 (unauthenticated)
vuln_a1 testConnection_status: 200
vuln_a1 testConnection_body: {"code":0}
vuln_a1 marker_exists: True
vuln_a1 marker_content: uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)

# Fixed 1.2.1 (unauthenticated)
fixed_a1 testConnection_status: 500
fixed_a1 testConnection_body: {"code":1002,"type":"RuntimeException",
  "message":"H2 JDBC URL is not allowed in catalog configuration",
  "stack":["...DataSourceUtils.createDataSource(DataSourceUtils.java:54)",
           "...JdbcCatalogOperations.initialize(JdbcCatalogOperations.java:175)",...]}
fixed_a1 marker_exists: False
```

Environment: OpenJDK 17.0.19, official Apache Gravitino binary tarballs,
H2 1.4.200 (bundled), Jetty 9, commons-dbcp2 2.11.0, default server config
(SimpleAuthenticator, authorization disabled, H2 entity-store backend).

## Recommendations / Next Steps

- **Upgrade** to Apache Gravitino 1.2.1 or later, which blocks H2 JDBC URLs
  and the `org.h2.` driver class in `DataSourceUtils.createDataSource()`.
- Consider a positive allow-list of permitted JDBC URL schemes/drivers in
  catalog configuration rather than only blocking H2, and apply the same
  check at catalog *creation* time (not only `testConnection`).
- In production deployments, enable authentication/authorization and use
  MySQL (not H2) for the entity store; restrict network exposure of the
  `testConnection` endpoint.
- Add regression tests that assert a `jdbc:h2:...;INIT=...` URL and
  `org.h2.Driver` are rejected through the full REST `testConnection` path.

## Additional Notes

- **Idempotency:** confirmed — the script was run twice consecutively; both
  runs exited 0 with identical verdicts (vulnerable RCE confirmed on both
  attempts, fixed blocked on both attempts). Each run extracts into fresh
  directories and uses a unique H2 in-memory DB name per attempt.
- **Payload detail:** every `;` inside the H2 `INIT` value (including Java
  statement terminators) must be escaped as `\;`, because H2 uses `;` as the
  URL parameter separator; unescaped `;` truncates the URL and the INIT
  never runs. The `jdbc-database` property must also be supplied for the
  `jdbc-postgresql` provider (`PostgreSqlSchemaOperations.initialize` reads
  it before the test connection is opened).
- The exploit does not require the catalog provider to be H2-aware: the H2
  driver is a "shared" class (`!isCatalogClass`) so the catalog
  `IsolatedClassLoader` delegates `org.h2.Driver` to the server classloader,
  which always has it.
