Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 0b0c1ce

Browse files
feat(jdbc): enforce strict JDBC URL parsing and sync DataSource properties (#4107)
* feat(jdbc): enforce strict JDBC URL parsing and validation * feat(jdbc): sync DataSource properties with BigQueryConnection * test: update tests for strict parsing * chore: fix failing tests * chore: throw original `BigQueryJdbcRuntimeException` * chore: add url encoding to appendProperties to prevent injection vulnerabilites * chore: refactor `parseAndRemovePartnerTokenProperty()` * fix: use "ProjectId" instead of "Project_Id" in tests * chore: add error message sanitization * chore: address pr feedback * fix: regex for partnerToken * chore: use `Joiner` and copy of map when setting properties
1 parent 895f7ef commit 0b0c1ce

6 files changed

Lines changed: 415 additions & 45 deletions

File tree

google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.bigquery.jdbc;
1818

1919
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
20+
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
2021
import io.grpc.LoadBalancerRegistry;
2122
import io.grpc.internal.PickFirstLoadBalancerProvider;
2223
import java.io.IOException;
@@ -123,6 +124,11 @@ public Connection connect(String url, Properties info) throws SQLException {
123124
// strip 'jdbc:' from the URL, add any extra properties
124125
String connectionUri =
125126
BigQueryJdbcUrlUtility.appendPropertiesToURL(url.substring(5), this.toString(), info);
127+
try {
128+
BigQueryJdbcUrlUtility.parseUrl(connectionUri);
129+
} catch (BigQueryJdbcRuntimeException e) {
130+
throw new BigQueryJdbcException(e.getMessage(), e);
131+
}
126132

127133
// LogLevel
128134
String logLevelStr =

google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919
import com.google.api.client.util.escape.CharEscapers;
2020
import com.google.cloud.bigquery.BigQueryOptions;
2121
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.net.UrlEscapers;
24+
import java.io.UnsupportedEncodingException;
25+
import java.net.URLDecoder;
26+
import java.nio.charset.StandardCharsets;
2227
import java.util.Arrays;
2328
import java.util.Collections;
2429
import java.util.HashMap;
2530
import java.util.HashSet;
31+
import java.util.LinkedHashMap;
2632
import java.util.List;
2733
import java.util.Map;
2834
import java.util.Map.Entry;
@@ -39,6 +45,14 @@
3945
*/
4046
final class BigQueryJdbcUrlUtility {
4147

48+
private static final Map<String, Map<String, String>> PARSE_CACHE =
49+
Collections.synchronizedMap(
50+
new LinkedHashMap<String, Map<String, String>>(50, 0.75f, true) {
51+
protected boolean removeEldestEntry(Map.Entry<String, Map<String, String>> eldest) {
52+
return size() > 50; // bound cache size
53+
}
54+
});
55+
4256
// TODO: Add all Connection options
4357
static final String ALLOW_LARGE_RESULTS_PROPERTY_NAME = "AllowLargeResults";
4458
static final String LARGE_RESULTS_TABLE_PROPERTY_NAME = "LargeResultTable";
@@ -122,6 +136,10 @@ final class BigQueryJdbcUrlUtility {
122136
static final String BYOID_SUBJECT_TOKEN_TYPE_PROPERTY_NAME = "BYOID_SubjectTokenType";
123137
static final String BYOID_TOKEN_URI_PROPERTY_NAME = "BYOID_TokenUri";
124138
static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken";
139+
private static final Pattern PARTNER_TOKEN_PATTERN =
140+
Pattern.compile(
141+
"(?:^|(?<=;))" + PARTNER_TOKEN_PROPERTY_NAME + "=\\s*((?:\\([^)]*\\)|[^;])*?)(?=(?:;|$))",
142+
Pattern.CASE_INSENSITIVE);
125143
static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount";
126144
static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32;
127145
static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout";
@@ -591,6 +609,37 @@ final class BigQueryJdbcUrlUtility {
591609
+ " header.")
592610
.build())));
593611

612+
private static final List<String> NETWORK_PROPERTIES =
613+
ImmutableList.of(
614+
PARTNER_TOKEN_PROPERTY_NAME,
615+
ENDPOINT_OVERRIDES_PROPERTY_NAME,
616+
PRIVATE_SERVICE_CONNECT_PROPERTY_NAME);
617+
618+
private static final Map<String, String> PROPERTY_NAME_MAP;
619+
620+
static {
621+
Map<String, String> map = new HashMap<>();
622+
for (BigQueryConnectionProperty p : VALID_PROPERTIES) {
623+
map.put(p.getName().toUpperCase(), p.getName());
624+
}
625+
for (BigQueryConnectionProperty p : AUTH_PROPERTIES) {
626+
map.put(p.getName().toUpperCase(), p.getName());
627+
}
628+
for (BigQueryConnectionProperty p : PROXY_PROPERTIES) {
629+
map.put(p.getName().toUpperCase(), p.getName());
630+
}
631+
for (String p : OVERRIDE_PROPERTIES) {
632+
map.put(p.toUpperCase(), p);
633+
}
634+
for (String p : BYOID_PROPERTIES) {
635+
map.put(p.toUpperCase(), p);
636+
}
637+
for (String p : NETWORK_PROPERTIES) {
638+
map.put(p.toUpperCase(), p);
639+
}
640+
PROPERTY_NAME_MAP = Collections.unmodifiableMap(map);
641+
}
642+
594643
private BigQueryJdbcUrlUtility() {}
595644

596645
/**
@@ -601,12 +650,69 @@ private BigQueryJdbcUrlUtility() {}
601650
* @return The String value of the property, or the default value if the property is not found.
602651
*/
603652
static String parseUriProperty(String uri, String property) {
604-
Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property));
605-
Matcher matcher = pattern.matcher(uri);
606-
if (matcher.find() && matcher.groupCount() == 1) {
607-
return CharEscapers.decodeUriPath(matcher.group(1));
653+
Map<String, String> map = parseUrl(uri);
654+
if (PROPERTY_NAME_MAP.containsKey(property.toUpperCase())) {
655+
return map.get(PROPERTY_NAME_MAP.get(property.toUpperCase()));
608656
}
609-
return null;
657+
return map.get(property);
658+
}
659+
660+
/**
661+
* Parses the URL into a map of key-value pairs, validating that all keys are known properties.
662+
*
663+
* @param url The URL to parse.
664+
* @return A map of property names to values.
665+
* @throws BigQueryJdbcRuntimeException if an unknown property is found or the URL is malformed.
666+
*/
667+
static Map<String, String> parseUrl(String url) {
668+
return PARSE_CACHE.computeIfAbsent(url, BigQueryJdbcUrlUtility::parseUrlInternal);
669+
}
670+
671+
private static Map<String, String> parseUrlInternal(String url) {
672+
Map<String, String> map = new HashMap<>();
673+
if (url == null) {
674+
return map;
675+
}
676+
677+
String[] urlParts = url.split(";", 2);
678+
if (urlParts.length < 2) {
679+
return map;
680+
}
681+
682+
String urlToParse = urlParts[1];
683+
684+
// Parse PartnerToken separately as it contains ';'
685+
Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlToParse);
686+
if (matcher.find()) {
687+
String rawToken = matcher.group(1).trim();
688+
String token =
689+
(rawToken.startsWith("(") && rawToken.endsWith(")"))
690+
? rawToken.substring(1, rawToken.length() - 1).trim()
691+
: rawToken;
692+
693+
if (token.toUpperCase().startsWith("GPN:")) {
694+
map.put(PARTNER_TOKEN_PROPERTY_NAME, " (" + token + ")");
695+
}
696+
urlToParse = matcher.replaceFirst("");
697+
}
698+
699+
String[] parts = urlToParse.split(";");
700+
for (String part : parts) {
701+
if (part.trim().isEmpty()) {
702+
continue;
703+
}
704+
String[] kv = part.split("=", 2);
705+
String key = kv[0].trim().toUpperCase();
706+
if (kv.length != 2 || !PROPERTY_NAME_MAP.containsKey(key)) {
707+
String ref = (kv.length == 2) ? key : part;
708+
String safeRef = ref.length() > 32 ? ref.substring(0, 32) + "..." : ref;
709+
throw new BigQueryJdbcRuntimeException(
710+
String.format("Wrong value or unknown setting: %s", safeRef));
711+
}
712+
713+
map.put(PROPERTY_NAME_MAP.get(key), CharEscapers.decodeUriPath(kv[1]));
714+
}
715+
return Collections.unmodifiableMap(map);
610716
}
611717

612718
/**
@@ -622,7 +728,11 @@ static String appendPropertiesToURL(String url, String callerClassName, Properti
622728
for (Entry<Object, Object> entry : properties.entrySet()) {
623729
if (entry.getValue() != null && !"".equals(entry.getValue())) {
624730
LOG.finest("Appending %s with value %s to URL", entry.getKey(), entry.getValue());
625-
urlBuilder.append(";").append(entry.getKey()).append("=").append(entry.getValue());
731+
String encodedValue =
732+
UrlEscapers.urlFormParameterEscaper()
733+
.escape((String) entry.getValue())
734+
.replace("+", "%20");
735+
urlBuilder.append(";").append(entry.getKey()).append("=").append(encodedValue);
626736
}
627737
}
628738
return urlBuilder.toString();
@@ -697,22 +807,17 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam
697807
LOG.finest("++enter++\t" + callerClassName);
698808
// This property is expected to be set by partners only. For more details on exact format
699809
// supported, refer b/396086960
700-
String regex =
701-
PARTNER_TOKEN_PROPERTY_NAME + "=\\s*\\(\\s*(GPN:[^;]*?)\\s*(?:;\\s*([^)]*?))?\\s*\\)";
702-
Pattern pattern = Pattern.compile(regex);
703-
Matcher matcher = pattern.matcher(url);
704-
810+
Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(url);
705811
if (matcher.find()) {
706-
String gpnPart = matcher.group(1);
707-
String environmentPart = matcher.group(2);
708-
StringBuilder partnerToken = new StringBuilder(" (");
709-
partnerToken.append(gpnPart);
710-
if (environmentPart != null && !environmentPart.trim().isEmpty()) {
711-
partnerToken.append("; ");
712-
partnerToken.append(environmentPart);
812+
String rawToken = matcher.group(1).trim();
813+
String token =
814+
(rawToken.startsWith("(") && rawToken.endsWith(")"))
815+
? rawToken.substring(1, rawToken.length() - 1).trim()
816+
: rawToken;
817+
818+
if (token.toUpperCase().startsWith("GPN:")) {
819+
return " (" + token + ")";
713820
}
714-
partnerToken.append(")");
715-
return partnerToken.toString();
716821
}
717822
return null;
718823
}
@@ -793,7 +898,12 @@ static Map<String, String> parseOverrideProperties(String url, String callerClas
793898
Matcher matcher = pattern.matcher(url);
794899
String overridePropertiesString;
795900
if (matcher.find() && matcher.groupCount() >= 1) {
796-
overridePropertiesString = matcher.group(2);
901+
try {
902+
overridePropertiesString =
903+
URLDecoder.decode(matcher.group(2), StandardCharsets.UTF_8.name());
904+
} catch (UnsupportedEncodingException e) {
905+
throw new BigQueryJdbcRuntimeException(e);
906+
}
797907
} else {
798908
return overrideProps;
799909
}

0 commit comments

Comments
 (0)