# Verification Report — CVE-2026-41042

## Fix Summary

Apache Gravitino (before 1.2.1) let an unauthenticated caller supply an
arbitrary JDBC URL through the `POST /api/metalakes/{ml}/catalogs/testConnection`
endpoint. A malicious `jdbc:h2:...;INIT=...` URL reached the H2 driver untouched
(the shared `JdbcUrlUtils.validateJdbcConfig` only inspects MySQL/MariaDB/PostgreSQL
URLs and intentionally skips H2, the internal entity-store backend). H2's `INIT`
connection parameter runs arbitrary SQL when the database is first opened, and via
`CREATE ALIAS ... AS '<java source>'` it compiles and executes arbitrary Java inside
the server JVM, yielding remote code execution.

The fix adds an explicit, case-insensitive, URL-decode-aware allow-list guard that
rejects H2 JDBC URLs and `org.h2.*` driver classes for **user-facing catalog
connections** — placed at the two JDBC-connection entry points reachable from the
unauthenticated `testConnection` API:

1. `DataSourceUtils.createDataSource(JdbcConfig)` — the sink used by every
   relational JDBC catalog provider (the original CVE path, fixed officially in
   1.2.1). This mirrors the upstream fix commit
   `5daabcd0e8ddc96e25bd6c6ce7b153cdb311c3f2` / cherry-pick `84d3de9c...`.
2. `CatalogUtils.checkPaimonConfig(PaimonConfig)` (JDBC-metastore branch) — the
   `lakehouse-paimon` JDBC backend opens its connection through Paimon's own
   `JdbcCatalog` (`DriverManager.getConnection`), **bypassing `DataSourceUtils`**,
   so the upstream fix alone does not protect it. This closes the bypass identified
   by the variant analysis.

Both guards throw `GravitinoRuntimeException` with the exact upstream messages
`"H2 JDBC URL is not allowed in catalog configuration"` /
`"H2 JDBC driver is not allowed in catalog configuration"`, so the runtime behavior
matches the fixed 1.2.1 server observed in the RCA evidence.

## Changes Made

`proposed_fix.diff` (2 files, +52/-0, applies cleanly to the v1.2.0 source tree):

- **`catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java`** (+16)
  - At the top of `createDataSource(JdbcConfig)`, before the DBCP pool is created,
    add: `recursiveDecode(jdbcUrl.toLowerCase())` is rejected if it `startsWith("jdbc:h2")`,
    and the driver class is rejected if it `startsWith("org.h2.")`. Both checks are
    case-insensitive; `recursiveDecode` (already present in the file, up to 5 rounds
    of `URLDecoder.decode`) defeats single/double URL-encoded prefix evasion. A null
    guard on the driver avoids a spurious NPE. This is the canonical 1.2.1 fix.

- **`catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/utils/CatalogUtils.java`** (+36)
  - In `checkPaimonConfig`, inside the `PaimonCatalogBackend.JDBC` branch (before
    `Class.forName(driverClassName)` and before `CatalogFactory.createCatalog` opens
    the connection), add the same H2 URL + `org.h2.*` driver rejection using the
    `uri`/`jdbc-driver` from the `PaimonConfig`. Adds a private `recursiveDecode`
    helper (mirroring the one in `DataSourceUtils`) and two imports
    (`java.net.URLDecoder`, `org.apache.gravitino.exceptions.GravitinoRuntimeException`).
  - Rationale: Paimon's `JdbcCatalog` calls `DriverManager.getConnection(uri, ...)`
    directly; H2's `INIT` fires there. The `DataSourceUtils` guard is never evaluated
    on this path, so the bypass is only closed by guarding here.

The internal H2 entity-store backend (`SqlSessionFactoryHelper`) does **not** go
through either of these entry points, so legitimate server operation is unaffected.

## Verification Steps

`bundle/coding/verify_fix.sh` is idempotent and self-contained. It derives the
Gravitino v1.2.0 source checkout and the official 1.2.0 server libraries from
`bundle/project_cache_context.json` and performs:

1. **Clean-apply check.** Reset the checkout to a clean `v1.2.0` tree
   (`git checkout -- . && git clean -fdq`), then `git apply --check proposed_fix.diff`
   and `git apply`. Confirms the patch is valid against the real vulnerable source.
2. **Compile + run the real `DataSourceUtils` guard.** `javac` compiles the patched
   `DataSourceUtils.java` + `JdbcConfig.java` + `H2BlockTest.java` against the 1.2.0
   server `libs/*.jar` (gravitino-api/common/core, dbcp2, commons-lang3, h2, …),
   then runs `H2BlockTest`, which calls the actual
   `DataSourceUtils.createDataSource(new JdbcConfig(props))`.
3. **Compile + run the real Paimon `CatalogUtils` guard.** Extract the
   `gravitino-catalog-lakehouse-paimon-1.2.0.jar`, drop the old `CatalogUtils.class`,
   and `javac` the patched `CatalogUtils.java` + `PaimonH2BlockTest.java` against the
   paimon + hadoop-common + gravitino jars. `PaimonH2BlockTest` invokes the (private)
   `checkPaimonConfig` via reflection with `PaimonConfig` built from the variant's
   exact properties.
4. **Reset** the checkout to clean v1.2.0 (idempotency).

Representative output (both runs identical, exit 0):

```
VERIFY OK:   checkout at v1.2.0, working tree clean
VERIFY OK:   git apply --check passed
VERIFY OK:   patch applied
     .../DataSourceUtils.java        | 16 ++++++++++
     .../CatalogUtils.java           | 36 ++++++++++++++++++++++
     2 files changed, 52 insertions(+)

PASS [blocked] cve-h2-url+org.h2.Driver -> H2 JDBC URL is not allowed in catalog configuration
PASS [blocked] variant-h2-url+org.h2.Driver -> H2 JDBC URL is not allowed in catalog configuration
PASS [blocked] encoded-jdbc%3Ah2 -> H2 JDBC URL is not allowed in catalog configuration
PASS [blocked] double-encoded-jdbc%253Ah2 -> H2 JDBC URL is not allowed in catalog configuration
PASS [blocked] upper-JDBC:H2 / mixed-Jdbc:H2 -> H2 JDBC URL is not allowed ...
PASS [blocked] driver-case-ORG.h2.Driver -> H2 JDBC driver is not allowed ...
PASS [blocked] org.h2.Driver-with-mysql-url / org.h2.Driver-plain -> H2 JDBC driver ...
PASS [allowed] postgresql / mysql / mariadb

PASS [blocked] paimon-jdbc-h2-uri+org.h2.Driver -> H2 JDBC URL is not allowed ...
PASS [blocked] paimon-jdbc-encoded-h2-uri / double-encoded / upper-JDBC:H2 -> H2 JDBC URL ...
PASS [blocked] paimon-jdbc-org.h2.Driver-with-pg-uri / ORG.h2.Driver-case -> H2 JDBC driver ...
PASS [allowed] paimon-filesystem-backend / paimon-jdbc-postgresql-driver

RESULT: ALL ASSERTIONS PASSED
VERIFY OK:   ALL VERIFICATION STEPS PASSED
```

The guard messages are byte-for-byte the ones the fixed 1.2.1 server returns in the
RCA evidence (`bundle/logs/fixed_attempt_*.log`), confirming behavior parity.

## Test Results

`H2BlockTest` (12 assertions) and `PaimonH2BlockTest` (8 assertions) — all PASS,
executed against the **real patched classes** compiled on the official 1.2.0
dependency classpath.

Edge cases covered:
- Original CVE payload: `jdbc:h2:mem:...;INIT=CREATE ALIAS EXEC AS '...'\;CALL EXEC()`
  + `org.h2.Driver` → blocked (URL).
- lakehouse-paimon variant payload (same H2 sink) → blocked (URL) on the Paimon path.
- URL-encoded prefix `jdbc%3Ah2%3Amem%3Atest` and double-encoded `jdbc%253Ah2...`
  → blocked (defeats encoding evasion via `recursiveDecode`).
- Case-insensitivity: `JDBC:H2`, `Jdbc:H2`, `ORG.h2.Driver` → blocked.
- H2 driver with a non-H2 URL (`jdbc:mysql://...` + `org.h2.Driver` /
  `org.h2.sub.Driver`) → blocked (driver).
- Legitimate configs **not** blocked: `jdbc:postgresql://`, `jdbc:mysql://`,
  `jdbc:mariadb://` (relational path) and `filesystem` backend / `jdbc:postgresql`
  driver (Paimon path) → no H2 guard fires.

Idempotency: `verify_fix.sh` was run twice consecutively; both runs exited 0 with
identical results and left the source checkout clean.

## Remaining Concerns

- **Scope of the bypass closure.** This fix guards the two concrete JDBC-connection
  entry points known to be reachable from the unauthenticated `testConnection` API
  (relational JDBC catalogs via `DataSourceUtils`, and `lakehouse-paimon` JDBC
  metastore via `CatalogUtils`). Other providers that open user-supplied JDBC
  connections through third-party code (outside `DataSourceUtils`) would still need
  the same guard; the variant analysis specifically flagged `lakehouse-paimon`, which
  is now covered. A longer-term hardening (per the variant recommendations) would be
  to centralize the H2/JDBC-URL allow-list in `JdbcUrlUtils` and route every
  provider's connection opening through it, while keeping a trusted-caller exemption
  for the internal entity store.
- **Deny-list vs. allow-list.** The fix denies H2 specifically (matching upstream).
  A positive allow-list of permitted JDBC URL schemes/drivers would be more robust
  against future driver-based gadget chains, but would be a larger behavioral change
  and is out of scope for this minimal security patch.
- **Authentication.** The endpoint is reachable unauthenticated in the default
  deployment (`SimpleAuthenticator`, `gravitino.authorization.enable=false`). This
  patch removes the code-execution primitive; enabling authentication/authorization
  remains recommended defense-in-depth.
- **Build verification.** The patched sources are verified by compiling them
  standalone against the official 1.2.0 server/paimon jars and exercising the guards
  at runtime (not by a full Gradle build), which is deterministic and fast in this
  environment. A full `./gradlew :catalogs:catalog-jdbc-common:compileJava
  :catalogs:catalog-lakehouse-paimon:compileJava` would additionally confirm
  integration into the Gradle build graph; the changes are additive (one guard block
  per method, two imports, one private helper) and introduce no new dependencies.
