# Patch Analysis — CVE-2026-41042 (Apache Gravitino H2 JDBC URL injection)

## 1. What the fix changes

**Fix commits:**
- main: `5daabcd0e8ddc96e25bd6c6ce7b153cdb311c3f2` — "[MINOR] fix(catalog): block H2 JDBC URL and driver in catalog datasource creation (#10801)"
- cherry-pick to `branch-1.2`: `84d3de9c7c436fbdac72b5f7a1163e65e0580fe2` (#10821), shipped in **Gravitino 1.2.1**.

**Files changed (3, +74/-1):**

1. `catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java` (+11): adds a guard at the very top of `createDataSource(JdbcConfig)`:

   ```java
   String decodedUrl = recursiveDecode(jdbcConfig.getJdbcUrl().toLowerCase());
   if (decodedUrl.startsWith("jdbc:h2")) {
     throw new GravitinoRuntimeException("H2 JDBC URL is not allowed in catalog configuration");
   }
   if (jdbcConfig.getJdbcDriver().toLowerCase().startsWith("org.h2.")) {
     throw new GravitinoRuntimeException("H2 JDBC driver is not allowed in catalog configuration");
   }
   ```

   plus a private `recursiveDecode()` helper (up to 5 rounds of `URLDecoder.decode`) so URL-encoded / double-encoded prefixes cannot hide `jdbc:h2`. Both the URL check and the driver check are case-insensitive (`toLowerCase()`).

2. `common/src/main/java/org/apache/gravitino/utils/JdbcUrlUtils.java` (-1): a one-line trim (unrelated whitespace), no behavioural change. `JdbcUrlUtils.validateJdbcConfig()` still only inspects MySQL/MariaDB/PostgreSQL URLs for known-unsafe params and intentionally does **not** touch H2 (H2 is the legitimate embedded entity-store backend).

3. `catalogs/catalog-jdbc-common/src/test/.../TestDataSourceUrlValidation.java` (+63): regression tests (`testRejectH2Url`, `testRejectH2UrlCaseInsensitive`, `testRejectH2Driver`, `testRejectH2DriverCaseInsensitive`, plus MySQL/PG/encoded cases).

## 2. What the fix assumes

The fix's central assumption, stated in the PR description:

> "Block H2 JDBC URLs and H2 driver class names in `DataSourceUtils.createDataSource()`, **which is the entry point for all user-facing catalog JDBC connections**." (emphasis added)

Concretely the fix assumes:

- **A1 — Single sink:** *Every* user-facing, attacker-controlled JDBC URL that can reach a bundled driver at connection time flows through `DataSourceUtils.createDataSource()`. Therefore blocking H2 there is sufficient.
- **A2 — H2 is the only dangerous bundled driver:** Only H2 can execute arbitrary code at connection time via URL parameters (its `INIT` → `CREATE ALIAS AS '<java>'` → compiled-and-invoked Java). No other bundled driver has an equivalent connect-time code-execution primitive.
- **A3 — Shared H2 driver is acceptable:** It is fine that `org.h2.Driver` is on the server classpath (entity store) and reachable from catalog classloaders, because the URL/driver block now prevents it from being used by a catalog.

## 3. What the fix does NOT cover (the gap this variant exploits)

**A1 is false.** `DataSourceUtils.createDataSource()` is the entry point for the *relational* JDBC catalog providers (`jdbc-mysql`, `jdbc-postgresql`, `jdbc-doris`, `jdbc-starrocks` — all of which extend `JdbcCatalogOperations`, whose `initialize()` calls `DataSourceUtils.createDataSource()`). It is **NOT** the entry point for catalog providers that maintain their own JDBC metastore backend and open JDBC connections through third-party library code.

The `lakehouse-paimon` provider is exactly such a case. When a user creates a Paimon catalog with `catalog-backend=jdbc`, the connection path is:

```
POST /api/metalakes/{ml}/catalogs/testConnection  (provider=lakehouse-paimon, unauthenticated)
  -> CatalogManager.testConnection
    -> BaseCatalog.ops()  (lazy initialize)
      -> PaimonCatalogOperations.initialize(conf, ...)                 [PaimonCatalogOperations.java:~120]
        -> new PaimonCatalogOps(new PaimonConfig(resultConf))          [PaimonCatalogOps.java:~48]
          -> CatalogUtils.loadCatalogBackend(paimonConfig)             [CatalogUtils.java:~59]
            -> CatalogUtils.loadCatalogBackendWithSimpleAuth(...)      [CatalogUtils.java:~115]
              -> checkPaimonConfig: Class.forName(driverClassName)     [CatalogUtils.java:~143]
              -> CatalogFactory.createCatalog(catalogContext)          (Paimon's own factory)
                -> org.apache.paimon.jdbc.JdbcCatalogFactory.create
                  -> org.apache.paimon.jdbc.JdbcCatalog.<init>         [JdbcCatalog.java:98]
                    -> DriverManager.getConnection(uri, user, pw)      *** H2 INIT fires here ***
```

This path **never calls `DataSourceUtils.createDataSource()`**, so the new H2 URL/driver block is never evaluated. Confirmed at runtime: across the full fixed-server log for the variant, the strings `"H2 JDBC URL is not allowed"` / `"H2 JDBC driver is not allowed"` appear **0 times**, while the request reaches `org.apache.paimon.jdbc.JdbcCatalog.<init>` (visible in the server stack trace).

**A3 is what makes the bypass reachable by default.** `core/.../utils/IsolatedClassLoader.isSharedClass()` delegates *every* non-Gravitino-catalog class to the server (base) classloader:

```java
private boolean isSharedClass(String name) {
  return name.startsWith("org.slf4j") || ... || name.startsWith("java.")
      || !isCatalogClass(name)                // <-- any non-catalog class is SHARED
      || sharedClasses.stream().anyMatch(name::startsWith);
}
```

`org.h2.Driver` is not a Gravitino catalog class, so the Paimon catalog's isolated classloader delegates `Class.forName("org.h2.Driver")` to the server classloader, which bundles `libs/h2-1.4.200.jar` (the entity-store backend). The H2 driver is therefore loadable from the Paimon provider with no extra jars, exactly as it was for the original `jdbc-postgresql` exploit.

## 4. Behaviour before vs. after the fix

| Path | 1.2.0 (vulnerable) | 1.2.1 (fixed) |
|------|--------------------|---------------|
| `provider=jdbc-postgresql`, `jdbc-url=jdbc:h2:...;INIT=...` (original CVE) | RCE (HTTP 200, marker written) | **Blocked**: `DataSourceUtils` throws "H2 JDBC URL is not allowed" (HTTP 500), no marker |
| `provider=lakehouse-paimon`, `catalog-backend=jdbc`, `uri=jdbc:h2:...;INIT=...` (**this variant**) | RCE (marker written) | **RCE — BYPASS**: `DataSourceUtils` block never runs; Paimon `JdbcCatalog` opens the H2 connection and `INIT` fires (marker written) |

## 5. Is the fix complete?

**No — the fix is incomplete.** It closes the original relational-JDBC entry point but leaves the identical H2 INIT/CREATE ALIAS sink reachable through the `lakehouse-paimon` JDBC metastore backend (and, by the same classloader logic, potentially any other catalog provider that opens JDBC connections via third-party code rather than `DataSourceUtils`). The fix's stated invariant ("DataSourceUtils is the entry point for *all* user-facing catalog JDBC connections") does not hold.

## 6. Target threat-model scope

Apache Gravitino treats unauthenticated REST access in the default deployment (SimpleAuthenticator + `gravitino.authorization.enable=false`) as a real exposure surface — the original CVE is rated on exactly that unauthenticated `testConnection` API surface, and the fix was shipped to protect it. The variant stays within that same trust boundary (same unauthenticated endpoint, same `testConnection`/catalog-create verbs), so it is in-scope and not a "documented limitation" or "local-file-user-attacks-self" case. No `SECURITY.md` exclusion applies to unauthenticated JDBC-URL-driven code execution.

## 7. Recommendation to close the gap

1. **Centralise the H2/JDBC-URL allow-list.** Move the H2 block (and ideally a positive allow-list of permitted JDBC URL schemes/drivers) out of `DataSourceUtils.createDataSource()` and into the shared `JdbcUrlUtils.validateJdbcConfig()` — but gated so the internal entity store (`SqlSessionFactoryHelper`) is still allowed to use H2 (e.g. skip the check for a trusted internal caller, or check a flag). Then ensure **every** provider that opens a JDBC connection from user-supplied properties calls it. Concretely, `CatalogUtils.checkPaimonConfig`/`loadCatalogBackendWithSimpleAuth` (Paimon) must validate `uri`/`jdbc-driver` before `CatalogFactory.createCatalog`.
2. **Apply the check at the property-validation layer** (`PropertiesMetadata` / `transformProperties`) for `lakehouse-paimon` so a `uri` starting with `jdbc:h2` (case-insensitive, URL-decoded) or a `jdbc-driver` starting with `org.h2.` is rejected for `catalog-backend=jdbc` — covering both `testConnection` and catalog creation.
3. **Audit other providers** that accept user JDBC URLs and open connections outside `DataSourceUtils` (e.g. any lakehouse provider with a JDBC backend, Iceberg `JdbcCatalog` if exposed, `JdbcPartitionStatisticStorageFactory`) and route them through the same validator.
4. Consider whether `org.h2.*` should remain a *shared* class for catalog classloaders at all (A3); restricting H2 to the entity-store classloader would also remove the primitive, but the URL/driver allow-list is the more general fix.
