{"repro_id":"REPRO-2026-00276","version":6,"title":"Apache Gravitino unauthenticated H2 JDBC URL injection via testConnection API","repro_type":"security","status":"published","severity":"low","description":"Unauthenticated callers can supply a malicious H2 JDBC URL through the testConnection API (or catalog creation) that executes arbitrary Java code on the server via H2's INIT parameter. The vulnerability is in DataSourceUtils.createDataSource() in gravitino-catalog-jdbc-common, which is the entry point for all user-facing catalog JDBC connections and did not validate/block H2 URLs or H2 driver class names. H2's INIT parameter allows arbitrary SQL (and Java code via CREATE ALIAS) to execute at connection time. Affects Apache Gravitino before 1.2.1.","root_cause":"# Root Cause Analysis — CVE-2026-41042\n\n## Summary\n\nApache Gravitino (before 1.2.1) exposes an unauthenticated REST endpoint\n`POST /api/metalakes/{metalake}/catalogs/testConnection` that lets a caller\nsupply an arbitrary JDBC URL for a catalog connection test. Because no\nvalidation rejected H2 JDBC URLs or the H2 driver class, an attacker could\nsupply a malicious `jdbc:h2:...;INIT=...` URL. H2's `INIT` connection\nparameter runs arbitrary SQL when the database is first opened, and H2's\n`CREATE ALIAS ... AS '<java source>'` compiles and executes arbitrary Java\ncode inside the Gravitino server JVM. By combining `CREATE ALIAS` with\n`Runtime`/`ProcessBuilder` calls, an unauthenticated remote caller achieves\narbitrary operating-system command execution on the server.\n\n## Impact\n\n- **Package/component affected:** `org.apache.gravitino:gravitino-catalog-jdbc-common`\n  (`DataSourceUtils.createDataSource`), reached through the\n  `testConnection` REST endpoint and the JDBC catalog operations\n  (`JdbcCatalogOperations.initialize`).\n- **Affected versions:** Apache Gravitino before 1.2.1 (confirmed on the\n  official `1.2.0` binary distribution; H2 `1.4.200` is bundled).\n- **Risk level / consequences:** Remote code execution. The default server\n  configuration uses the `SimpleAuthenticator` (accepts anonymous requests)\n  and has `gravitino.authorization.enable = false`, so the endpoint is\n  reachable without any credentials. The H2 driver is always on the server\n  classpath (it is the default entity-store backend) and is treated as a\n  \"shared\" class by the catalog `IsolatedClassLoader`, so attacker-supplied\n  H2 URLs resolve to the bundled driver even for non-H2 catalog providers.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Remote code execution —\n  \"executes arbitrary Java code on the server via H2's INIT parameter\",\n  expected impact `code_execution`.\n- **Reproduced impact from this run:** Arbitrary OS command execution\n  confirmed end-to-end through the unauthenticated REST API. The malicious\n  `testConnection` call returned HTTP 200 / `{\"code\":0}` and a marker file\n  containing the output of the `id` command\n  (`uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)`) was written by\n  a `ProcessBuilder` started from H2-compiled Java code running in the\n  server JVM.\n- **Parity:** `full`.\n- **Not demonstrated:** N/A — the full code-execution chain was demonstrated.\n\n## Root Cause\n\n`DataSourceUtils.createDataSource(JdbcConfig)` is the single entry point for\nall user-facing catalog JDBC connections. In the vulnerable version it\nperformed no allow-listing of JDBC drivers/URLs: it passed the caller-\ncontrolled `jdbc-url` and `jdbc-driver` straight into a DBCP\n`BasicDataSource`. The shared validator `JdbcUrlUtils.validateJdbcConfig`\nonly inspects MySQL/MariaDB/PostgreSQL URLs for unsafe parameters and\nintentionally does **not** touch H2 (H2 is the legitimate embedded\nentity-store backend). Consequently an H2 URL reached the driver untouched.\n\nThe call chain triggered by `testConnection` is:\n\n```\nCatalogOperations.testConnection (REST, @Path \"testConnection\")\n  -> CatalogManager.testConnection\n    -> CatalogWrapper.doWithCatalogOps\n      -> BaseCatalog.ops()  (lazy initialize)\n        -> JdbcCatalogOperations.initialize(conf, ...)\n          -> DataSourceUtils.createDataSource(jdbcConfig)   <-- vulnerable\n            -> BasicDataSource (url=jdbc:h2:mem:...;INIT=..., driver=org.h2.Driver)\n```\n\nWhen the pool opens its first connection (during\n`JdbcCatalogOperations.testConnection -> databaseOperation.listDatabases()`),\nH2 processes the `INIT` parameter. The payload\n`INIT=CREATE ALIAS EXEC AS 'String f() throws Exception { new ProcessBuilder(...).start().waitFor(); ... }'\\;CALL EXEC()`\ncompiles the Java source via the JDK compiler and invokes it, executing the\nattacker's OS command inside the server process.\n\n**Fix commit:** `5daabcd0e8ddc96e25bd6c6ce7b153cdb311c3f2`\n(\"[MINOR] fix(catalog): block H2 JDBC URL and driver in catalog datasource\ncreation (#10801)\"), cherry-picked to the 1.2 release branch as\n`84d3de9c7` and shipped in Gravitino 1.2.1. The fix adds a case-insensitive\nblock in `DataSourceUtils.createDataSource()` that rejects any URL whose\ndecoded form starts with `jdbc:h2` and any driver class starting with\n`org.h2.`, throwing `GravitinoRuntimeException` before the datasource is\ncreated.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained,\n   idempotent, run twice consecutively — both runs pass with exit 0).\n2. **What it does:**\n   - Installs OpenJDK 17 if needed.\n   - Downloads/reuses the official Apache Gravitino binary distributions\n     `gravitino-1.2.0-bin.tar.gz` (vulnerable) and\n     `gravitino-1.2.1-bin.tar.gz` (fixed) from the GitHub release CDN\n     (fallback: Apache archive).\n   - For each version: extracts a clean copy, disables auxiliary services,\n     starts the real `gravitino.sh` server, waits for the `/api/version`\n     healthcheck, creates a metalake, and sends an **unauthenticated**\n     `POST /api/metalakes/test_ml/catalogs/testConnection` with\n     `provider=jdbc-postgresql`, `jdbc-driver=org.h2.Driver`, and a\n     malicious `jdbc-url` whose `INIT` runs `CREATE ALIAS` + `ProcessBuilder`\n     to execute `id` and write its output to a marker file. Two attempts are\n     made per version (each attempt uses a fresh H2 in-memory DB name because\n     H2 only runs `INIT` when a database is first created).\n   - Captures per-attempt request/response logs, server logs, version JSON,\n     and the RCE marker file under `bundle/logs/`.\n   - Writes `bundle/repro/runtime_manifest.json`.\n3. **Expected evidence:**\n   - Vulnerable 1.2.0: `testConnection` returns HTTP 200 `{\"code\":0}` and the\n     marker file is created containing `id` output → RCE.\n   - Fixed 1.2.1: `testConnection` returns HTTP 500\n     `\"H2 JDBC URL is not allowed in catalog configuration\"` (thrown at\n     `DataSourceUtils.createDataSource:54`) and no marker file is created.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — orchestrator log with verdict.\n- `bundle/logs/vuln_version.json` — `1.2.0`, gitCommit `1c1665f0...`.\n- `bundle/logs/fixed_version.json` — `1.2.1`, gitCommit `e88bffcfc...`.\n- `bundle/logs/vuln_attempt_1.log` / `vuln_attempt_2.log` —\n  `testConnection_status: 200`, `testConnection_body: {\"code\":0}`,\n  `marker_exists: True`, `marker_content: uid=1000(vscode) ...`.\n- `bundle/logs/rce_marker_vuln_latest.txt` — the file written by the\n  attacker's OS command inside the server JVM.\n- `bundle/logs/fixed_attempt_1.log` / `fixed_attempt_2.log` —\n  `testConnection_status: 500`, message\n  `\"H2 JDBC URL is not allowed in catalog configuration\"`, stack at\n  `DataSourceUtils.createDataSource(DataSourceUtils.java:54)`,\n  `marker_exists: False`.\n- `bundle/logs/vuln_server.log` / `fixed_server.log` — server-side logs.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest.\n\nKey excerpts:\n\n```\n# Vulnerable 1.2.0 (unauthenticated)\nvuln_a1 testConnection_status: 200\nvuln_a1 testConnection_body: {\"code\":0}\nvuln_a1 marker_exists: True\nvuln_a1 marker_content: uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)\n\n# Fixed 1.2.1 (unauthenticated)\nfixed_a1 testConnection_status: 500\nfixed_a1 testConnection_body: {\"code\":1002,\"type\":\"RuntimeException\",\n  \"message\":\"H2 JDBC URL is not allowed in catalog configuration\",\n  \"stack\":[\"...DataSourceUtils.createDataSource(DataSourceUtils.java:54)\",\n           \"...JdbcCatalogOperations.initialize(JdbcCatalogOperations.java:175)\",...]}\nfixed_a1 marker_exists: False\n```\n\nEnvironment: OpenJDK 17.0.19, official Apache Gravitino binary tarballs,\nH2 1.4.200 (bundled), Jetty 9, commons-dbcp2 2.11.0, default server config\n(SimpleAuthenticator, authorization disabled, H2 entity-store backend).\n\n## Recommendations / Next Steps\n\n- **Upgrade** to Apache Gravitino 1.2.1 or later, which blocks H2 JDBC URLs\n  and the `org.h2.` driver class in `DataSourceUtils.createDataSource()`.\n- Consider a positive allow-list of permitted JDBC URL schemes/drivers in\n  catalog configuration rather than only blocking H2, and apply the same\n  check at catalog *creation* time (not only `testConnection`).\n- In production deployments, enable authentication/authorization and use\n  MySQL (not H2) for the entity store; restrict network exposure of the\n  `testConnection` endpoint.\n- Add regression tests that assert a `jdbc:h2:...;INIT=...` URL and\n  `org.h2.Driver` are rejected through the full REST `testConnection` path.\n\n## Additional Notes\n\n- **Idempotency:** confirmed — the script was run twice consecutively; both\n  runs exited 0 with identical verdicts (vulnerable RCE confirmed on both\n  attempts, fixed blocked on both attempts). Each run extracts into fresh\n  directories and uses a unique H2 in-memory DB name per attempt.\n- **Payload detail:** every `;` inside the H2 `INIT` value (including Java\n  statement terminators) must be escaped as `\\;`, because H2 uses `;` as the\n  URL parameter separator; unescaped `;` truncates the URL and the INIT\n  never runs. The `jdbc-database` property must also be supplied for the\n  `jdbc-postgresql` provider (`PostgreSqlSchemaOperations.initialize` reads\n  it before the test connection is opened).\n- The exploit does not require the catalog provider to be H2-aware: the H2\n  driver is a \"shared\" class (`!isCatalogClass`) so the catalog\n  `IsolatedClassLoader` delegates `org.h2.Driver` to the server classloader,\n  which always has it.\n","cve_id":"CVE-2026-41042","source_url":"https://github.com/apache/gravitino","package":{"name":"apache/gravitino","ecosystem":"github","affected_versions":"Apache Gravitino before 1.2.1","fixed_version":"1.2.1"},"reproduced_at":"2026-07-09T14:28:23.013513+00:00","duration_secs":1367.0,"tool_calls":308,"handoffs":3,"total_cost_usd":5.324745309999999,"agent_costs":{"coding":1.0479264000000004,"hypothesis_generator":0.013151,"judge":0.0179381,"repro":2.7885526800000004,"support":0.10793163,"vuln_variant":1.3492455},"cost_breakdown":{"coding":{"accounts/fireworks/routers/glm-5p2-fast":1.0479264000000004},"hypothesis_generator":{"accounts/fireworks/models/glm-5p2":0.013151},"judge":{"gpt-5.4-mini":0.0179381},"repro":{"accounts/fireworks/routers/glm-5p2-fast":2.7885526800000004},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.10793163},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.3492455}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-09T14:28:52.678159+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12713,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9508,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12241,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":15021,"category":"analysis"},{"path":"bundle/coding/proposed_fix.diff","filename":"proposed_fix.diff","size":5054,"category":"patch"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":13180,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":18135,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":2369,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":3067,"category":"other"},{"path":"bundle/coding/H2BlockTest.java","filename":"H2BlockTest.java","size":4825,"category":"other"},{"path":"bundle/coding/PaimonH2BlockTest.java","filename":"PaimonH2BlockTest.java","size":4915,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1024,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":2008,"category":"other"},{"path":"bundle/logs/rce_marker_vuln_latest.txt","filename":"rce_marker_vuln_latest.txt","size":54,"category":"other"},{"path":"bundle/logs/vuln_attempt_1.log","filename":"vuln_attempt_1.log","size":331,"category":"log"},{"path":"bundle/logs/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":648,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":1590,"category":"log"},{"path":"bundle/logs/vuln_version.json","filename":"vuln_version.json","size":131,"category":"other"},{"path":"bundle/logs/vuln_attempt_2.log","filename":"vuln_attempt_2.log","size":331,"category":"log"},{"path":"bundle/logs/vuln_server.log","filename":"vuln_server.log","size":34056,"category":"log"},{"path":"bundle/logs/fixed_version.json","filename":"fixed_version.json","size":131,"category":"other"},{"path":"bundle/logs/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":648,"category":"log"},{"path":"bundle/logs/fixed_server.log","filename":"fixed_server.log","size":33436,"category":"log"},{"path":"bundle/logs/vuln_variant/rce_marker_paimon_fixed_latest.txt","filename":"rce_marker_paimon_fixed_latest.txt","size":54,"category":"other"},{"path":"bundle/logs/vuln_variant/paimon_fixed_server.log","filename":"paimon_fixed_server.log","size":62690,"category":"log"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2614,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":5471,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":9117,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":3949,"category":"other"},{"path":"bundle/coding/verify_fix.sh","filename":"verify_fix.sh","size":5581,"category":"other"},{"path":"bundle/coding/summary_report.md","filename":"summary_report.md","size":9611,"category":"documentation"}]}