From e5d304767f8c00a9a1c53b27972989a37283d7c3 Mon Sep 17 00:00:00 2001 From: Sreekanth Vadigi Date: Thu, 18 Jun 2026 11:21:52 +0000 Subject: [PATCH] Redact credentials in DatabaseMetaData.getURL() and parse errors getURL() returned the connection URL verbatim, exposing any secret embedded in it (PWD, Auth_AccessToken, OAuth2Secret, etc.); the same raw URL was embedded in parse-error messages. Secret parameter values are now masked while the URL is otherwise unchanged. The internal raw URL and all connection/auth/query flows are untouched. Co-authored-by: Isaac Signed-off-by: Sreekanth Vadigi --- NEXT_CHANGELOG.md | 1 + .../api/impl/DatabricksConnectionContext.java | 44 ++++++++++++++++++- .../api/impl/DatabricksDatabaseMetaData.java | 3 +- .../impl/DatabricksConnectionContextTest.java | 34 ++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index a972de65e..dd80b5922 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### Updated ### Fixed +- Fixed `DatabaseMetaData.getURL()` exposing credentials embedded in the connection URL; secret parameters are now masked (the URL is otherwise unchanged). - Fixed `setCatalog()` and `setSchema()` producing invalid SQL (e.g. `SET CATALOG ``name``) when the catalog or schema name was passed already wrapped in backticks. Backticks are now stripped before wrapping, and `getCatalog()`/`getSchema()` return the bare identifier name. - Fixed metadata SQL generation for catalog, schema, and table identifiers containing backticks. - Fixed SEA result truncation when direct results are disabled. Large, highly-compressible results that span multiple chunks were delivered inline via the old hybrid path and truncated to the first chunk. The SQL Execution path now uses an async (`0s`) wait timeout when direct results are disabled, so results are returned via external links and fetched in full. diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java index 956695ab5..6d4048076 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.regex.Matcher; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; @@ -158,7 +159,7 @@ public static IDatabricksConnectionContext parse(String url, Properties properti throws DatabricksSQLException { if (!ValidationUtil.isValidJdbcUrl(url)) { throw new DatabricksParsingException( - "Invalid url " + url, DatabricksDriverErrorCode.CONNECTION_ERROR); + "Invalid url " + redactConnectionURL(url), DatabricksDriverErrorCode.CONNECTION_ERROR); } Matcher urlMatcher = JDBC_URL_PATTERN.matcher(url); if (urlMatcher.find()) { @@ -778,6 +779,47 @@ public String getConnectionURL() { return connectionURL; } + // Connection-URL params whose values are secrets and must be masked before the URL is exposed + // (DatabaseMetaData.getURL(), exceptions, logs, telemetry). + private static final Set SENSITIVE_URL_PARAMS = + Stream.of( + DatabricksJdbcUrlParams.PWD, + DatabricksJdbcUrlParams.PASSWORD, + DatabricksJdbcUrlParams.CLIENT_SECRET, + DatabricksJdbcUrlParams.AUTH_ACCESS_TOKEN, + DatabricksJdbcUrlParams.OAUTH_REFRESH_TOKEN, + DatabricksJdbcUrlParams.OAUTH_REFRESH_TOKEN_2, + DatabricksJdbcUrlParams.PROXY_PWD, + DatabricksJdbcUrlParams.CF_PROXY_PWD, + DatabricksJdbcUrlParams.SSL_KEY_STORE_PASSWORD, + DatabricksJdbcUrlParams.SSL_TRUST_STORE_PASSWORD, + DatabricksJdbcUrlParams.TOKEN_CACHE_PASS_PHRASE, + DatabricksJdbcUrlParams.JWT_PASS_PHRASE) + .map(p -> p.getParamName().toLowerCase()) + .collect(Collectors.toSet()); + + /** Masks the values of secret-bearing parameters in a JDBC connection URL. */ + public static String redactConnectionURL(String url) { + if (url == null) { + return null; + } + String[] parts = url.split(URL_DELIMITER); + StringBuilder sb = new StringBuilder(url.length()); + for (int i = 0; i < parts.length; i++) { + if (i > 0) { + sb.append(URL_DELIMITER); + } + String part = parts[i]; + int idx = part.indexOf(PAIR_DELIMITER); + if (idx > 0 && SENSITIVE_URL_PARAMS.contains(part.substring(0, idx).toLowerCase())) { + sb.append(part, 0, idx + 1).append(REDACTED_TOKEN); + } else { + sb.append(part); + } + } + return sb.toString(); + } + @Override public boolean checkCertificateRevocation() { return Objects.equals(getParameter(DatabricksJdbcUrlParams.CHECK_CERTIFICATE_REVOCATION), "1"); diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksDatabaseMetaData.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksDatabaseMetaData.java index a53b228c7..fb78a6721 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksDatabaseMetaData.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksDatabaseMetaData.java @@ -64,7 +64,8 @@ public boolean allTablesAreSelectable() throws SQLException { @Override public String getURL() throws SQLException { LOGGER.debug("public String getURL()"); - return this.session.getConnectionContext().getConnectionURL(); + return DatabricksConnectionContext.redactConnectionURL( + this.session.getConnectionContext().getConnectionURL()); } @Override diff --git a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java index f207c1537..6e3593035 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java @@ -87,6 +87,40 @@ public void testParseInvalid() { () -> DatabricksConnectionContext.parse(TestConstants.INVALID_URL_2, properties)); } + @Test + public void testRedactConnectionURL_masksSecretsKeepsRest() { + String url = + "jdbc:databricks://host.databricks.com:443/default;transportMode=https;ssl=1;" + + "AuthMech=3;httpPath=/sql/1.0/warehouses/abc;UID=token;" + + "PWD=dapiSECRET;OAuth2Secret=clientSecretVal;Auth_AccessToken=tokenVal"; + + String redacted = DatabricksConnectionContext.redactConnectionURL(url); + + // Secrets masked. + assertFalse(redacted.contains("dapiSECRET")); + assertFalse(redacted.contains("clientSecretVal")); + assertFalse(redacted.contains("tokenVal")); + assertTrue(redacted.contains("PWD=****")); + assertTrue(redacted.contains("OAuth2Secret=****")); + assertTrue(redacted.contains("Auth_AccessToken=****")); + // Non-secret params and host preserved. + assertTrue(redacted.contains("jdbc:databricks://host.databricks.com:443/default")); + assertTrue(redacted.contains("httpPath=/sql/1.0/warehouses/abc")); + assertTrue(redacted.contains("UID=token")); + } + + @Test + public void testRedactConnectionURL_caseInsensitiveKeyAndNullSafe() { + assertNull(DatabricksConnectionContext.redactConnectionURL(null)); + // Key match is case-insensitive (driver lowercases param keys when parsing). + String redacted = + DatabricksConnectionContext.redactConnectionURL( + "jdbc:databricks://h:443/default;pwd=secret;ProxyPwd=proxySecret"); + assertFalse(redacted.contains("secret")); + assertTrue(redacted.contains("pwd=****")); + assertTrue(redacted.contains("ProxyPwd=****")); + } + @Test public void testParseValid() throws DatabricksSQLException { // test provided port