-
Notifications
You must be signed in to change notification settings - Fork 2.8k
[ZEPPELIN-6204] do not allow init params in JDBC URLs for H2 #4949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a10fe53
0ab3fb3
7b5e2f0
8c21863
9353662
948cc34
362ad96
996bdd1
0d78f8b
b4e694a
26def28
dddbec3
fd38a07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -156,14 +156,22 @@ public class JDBCInterpreter extends KerberosInterpreter { | |||||||||||||||||||||||||
"KerberosConfigPath", "KerberosKeytabPath", "KerberosCredentialCachePath", | ||||||||||||||||||||||||||
"extraCredentials", "roles", "sessionProperties")); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String ALLOW_LOAD_LOCAL = "allowLoadLocal"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String ALLOW_LOAD_LOCAL_IN_FILE_NAME = "allowLoadLocalInfile"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String AUTO_DESERIALIZE = "autoDeserialize"; | ||||||||||||||||||||||||||
private static final String ALLOW_LOAD_LOCAL_IN_FILE_IN_PATH = "allowLoadLocalInfileInPath"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String ALLOW_LOCAL_IN_FILE_NAME = "allowLocalInfile"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String ALLOW_URL_IN_LOCAL_IN_FILE_NAME = "allowUrlInLocalInfile"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String AUTO_DESERIALIZE = "autoDeserialize"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String SOCKET_FACTORY = "socketFactory"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String INIT = "INIT"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// database --> Properties | ||||||||||||||||||||||||||
private final HashMap<String, Properties> basePropertiesMap; | ||||||||||||||||||||||||||
// username --> User Configuration | ||||||||||||||||||||||||||
|
@@ -588,18 +596,127 @@ public Connection getConnection(InterpreterContext context) | |||||||||||||||||||||||||
return connection; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private void validateConnectionUrl(String url) { | ||||||||||||||||||||||||||
String decodedUrl; | ||||||||||||||||||||||||||
decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8); | ||||||||||||||||||||||||||
// package private for testing purposes | ||||||||||||||||||||||||||
static void validateConnectionUrl(String url) { | ||||||||||||||||||||||||||
final String decodedUrl = urlDecode(url, url, 0); | ||||||||||||||||||||||||||
final Map<String, String> params = parseUrlParameters(decodedUrl); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (containsKeyIgnoreCase(params, ALLOW_LOAD_LOCAL) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, ALLOW_LOAD_LOCAL_IN_FILE_NAME) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, ALLOW_LOCAL_IN_FILE_NAME) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, ALLOW_URL_IN_LOCAL_IN_FILE_NAME) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, ALLOW_LOAD_LOCAL_IN_FILE_IN_PATH) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, AUTO_DESERIALIZE) || | ||||||||||||||||||||||||||
containsKeyIgnoreCase(params, SOCKET_FACTORY)) { | ||||||||||||||||||||||||||
throw new IllegalArgumentException("Connection URL contains sensitive configuration"); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
Comment on lines
+604
to
+612
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I find this version easier to read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't commit this unless it compiles so please don't use the suggestion option to suggest incomplete code. |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (containsIgnoreCase(decodedUrl, ALLOW_LOAD_LOCAL_IN_FILE_NAME) || | ||||||||||||||||||||||||||
containsIgnoreCase(decodedUrl, AUTO_DESERIALIZE) || | ||||||||||||||||||||||||||
containsIgnoreCase(decodedUrl, ALLOW_LOCAL_IN_FILE_NAME) || | ||||||||||||||||||||||||||
containsIgnoreCase(decodedUrl, ALLOW_URL_IN_LOCAL_IN_FILE_NAME)) { | ||||||||||||||||||||||||||
// the INIT parameter name is a bit generic so we check it only for H2 | ||||||||||||||||||||||||||
if (containsIgnoreCase(decodedUrl, "jdbc:h2") && containsKeyIgnoreCase(params, INIT)) { | ||||||||||||||||||||||||||
throw new IllegalArgumentException("Connection URL contains sensitive configuration"); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* Decode the URL encoded string recursively until no more decoding is needed. | ||||||||||||||||||||||||||
* This is to handle cases where the URL might be double-encoded. | ||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||
* @param url the original URL (for logging purposes) | ||||||||||||||||||||||||||
* @param encoded the URL encoded string | ||||||||||||||||||||||||||
* @param recurseCount the current recursion depth | ||||||||||||||||||||||||||
* @return the decoded string | ||||||||||||||||||||||||||
* @throws IllegalArgumentException if the recursion depth exceeds 10 | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
private static String urlDecode(final String url, | ||||||||||||||||||||||||||
final String encoded, | ||||||||||||||||||||||||||
final int recurseCount) { | ||||||||||||||||||||||||||
if (recurseCount > 10) { | ||||||||||||||||||||||||||
throw new IllegalArgumentException("illegal URL encoding detected: " + url); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
final String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8); | ||||||||||||||||||||||||||
if (decoded.equals(encoded)) { | ||||||||||||||||||||||||||
return decoded; // No more decoding needed or max recursion reached | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return urlDecode(url, decoded, recurseCount + 1); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static Map<String, String> parseUrlParameters(final String url) { | ||||||||||||||||||||||||||
final Map<String, String> parameters = new HashMap<>(); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// MySQL supports parentheses in the URL | ||||||||||||||||||||||||||
// https://dev.mysql.com/doc/connectors/en/connector-j-reference-jdbc-url-format.html | ||||||||||||||||||||||||||
// eg jdbc:mysql://(host=myhost,port=1111,allowLoadLocalInfile=true)/db | ||||||||||||||||||||||||||
int parensIndex = extractFromParens(url, 0, parameters); | ||||||||||||||||||||||||||
while (parensIndex > 0) { | ||||||||||||||||||||||||||
parensIndex = extractFromParens(url, parensIndex, parameters); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Split the URL into the base part and the parameters part | ||||||||||||||||||||||||||
String[] parts = url.split("[?&;]"); | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be a bit faster if the regex was cached to avoid rebuilding every time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the cost of this regex will be tiny compared to the I/O cost of using JDBC commands so I'm not very interested in optimising every line of code here |
||||||||||||||||||||||||||
if (parts.length > 1) { | ||||||||||||||||||||||||||
// The first part is the base URL, so we start from the second part | ||||||||||||||||||||||||||
for (int i = 1; i < parts.length; i++) { | ||||||||||||||||||||||||||
splitNameValue(parts[i], parameters, true); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return parameters; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static boolean containsKeyIgnoreCase(Map<String, String> map, String key) { | ||||||||||||||||||||||||||
for (String k : map.keySet()) { | ||||||||||||||||||||||||||
if (k.equalsIgnoreCase(key)) { | ||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* Extracts key-value pairs from parentheses in the input string. | ||||||||||||||||||||||||||
* The expected format is "(key1=value1, key2=value2, ...)". | ||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||
* @param input the input string containing parameters in parentheses | ||||||||||||||||||||||||||
* @param initIndex the index to start searching for parentheses | ||||||||||||||||||||||||||
* @param parameters the map to store extracted key-value pairs | ||||||||||||||||||||||||||
* @return the index of the closing parenthesis or -1 if not found | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
private static int extractFromParens(final String input, | ||||||||||||||||||||||||||
final int initIndex, | ||||||||||||||||||||||||||
final Map<String, String> parameters) { | ||||||||||||||||||||||||||
final int startIndex = input.indexOf('(', initIndex); | ||||||||||||||||||||||||||
if (startIndex == -1) { | ||||||||||||||||||||||||||
return -1; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
final int endIndex = input.indexOf(')', startIndex); | ||||||||||||||||||||||||||
if (startIndex != -1 && endIndex != -1) { | ||||||||||||||||||||||||||
String params = input.substring(startIndex + 1, endIndex); | ||||||||||||||||||||||||||
String[] keyValuePairs = params.split(","); | ||||||||||||||||||||||||||
for (String pair : keyValuePairs) { | ||||||||||||||||||||||||||
splitNameValue(pair, parameters, false); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it work well if parentheses are in the URL query parameters? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the code doesn't care about what the values are, it only cares about the param names - write some test cases if you are suspicious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested the following cases: testBannedMySQLQueryParamWithValidParam("password=allowLoadLocalInfile"); // 1. fail
testBannedMySQLQueryParamWithValidParam("password=(allowLoadLocalInfile)"); // 2. pass I might be missing something, but test case 2 seems unintended. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks - I'll look into the issue - but in the end of the day, it's not a very likely password |
||||||||||||||||||||||||||
return endIndex; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* Splits a name-value pair and adds it to the parameters map. | ||||||||||||||||||||||||||
* Handles cases where the value might be missing. | ||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||
* @param nameValue the name-value pair as a string | ||||||||||||||||||||||||||
* @param parameters the map to store the extracted key-value pair | ||||||||||||||||||||||||||
* @param allowEmptyValue whether to allow empty values | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
private static void splitNameValue(String nameValue, Map<String, String> parameters, | ||||||||||||||||||||||||||
boolean allowEmptyValue) { | ||||||||||||||||||||||||||
String[] keyValue = nameValue.split("="); | ||||||||||||||||||||||||||
if (keyValue.length >= 2) { | ||||||||||||||||||||||||||
parameters.put(keyValue[0].trim(), keyValue[1].trim()); | ||||||||||||||||||||||||||
} else if (allowEmptyValue) { | ||||||||||||||||||||||||||
// Handle cases where there might not be a value | ||||||||||||||||||||||||||
parameters.put(keyValue[0].trim(), ""); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private String appendProxyUserToURL(String url, String user) { | ||||||||||||||||||||||||||
StringBuilder connectionUrl = new StringBuilder(url); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be nice to group these into a Set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to stick with the existing code style.