@@ -156,14 +156,22 @@ public class JDBCInterpreter extends KerberosInterpreter {
156156 "KerberosConfigPath" , "KerberosKeytabPath" , "KerberosCredentialCachePath" ,
157157 "extraCredentials" , "roles" , "sessionProperties" ));
158158
159+ private static final String ALLOW_LOAD_LOCAL = "allowLoadLocal" ;
160+
159161 private static final String ALLOW_LOAD_LOCAL_IN_FILE_NAME = "allowLoadLocalInfile" ;
160162
161- private static final String AUTO_DESERIALIZE = "autoDeserialize " ;
163+ private static final String ALLOW_LOAD_LOCAL_IN_FILE_IN_PATH = "allowLoadLocalInfileInPath " ;
162164
163165 private static final String ALLOW_LOCAL_IN_FILE_NAME = "allowLocalInfile" ;
164166
165167 private static final String ALLOW_URL_IN_LOCAL_IN_FILE_NAME = "allowUrlInLocalInfile" ;
166168
169+ private static final String AUTO_DESERIALIZE = "autoDeserialize" ;
170+
171+ private static final String SOCKET_FACTORY = "socketFactory" ;
172+
173+ private static final String INIT = "INIT" ;
174+
167175 // database --> Properties
168176 private final HashMap <String , Properties > basePropertiesMap ;
169177 // username --> User Configuration
@@ -588,18 +596,127 @@ public Connection getConnection(InterpreterContext context)
588596 return connection ;
589597 }
590598
591- private void validateConnectionUrl (String url ) {
592- String decodedUrl ;
593- decodedUrl = URLDecoder .decode (url , StandardCharsets .UTF_8 );
599+ // package private for testing purposes
600+ static void validateConnectionUrl (String url ) {
601+ final String decodedUrl = urlDecode (url , url , 0 );
602+ final Map <String , String > params = parseUrlParameters (decodedUrl );
603+
604+ if (containsKeyIgnoreCase (params , ALLOW_LOAD_LOCAL ) ||
605+ containsKeyIgnoreCase (params , ALLOW_LOAD_LOCAL_IN_FILE_NAME ) ||
606+ containsKeyIgnoreCase (params , ALLOW_LOCAL_IN_FILE_NAME ) ||
607+ containsKeyIgnoreCase (params , ALLOW_URL_IN_LOCAL_IN_FILE_NAME ) ||
608+ containsKeyIgnoreCase (params , ALLOW_LOAD_LOCAL_IN_FILE_IN_PATH ) ||
609+ containsKeyIgnoreCase (params , AUTO_DESERIALIZE ) ||
610+ containsKeyIgnoreCase (params , SOCKET_FACTORY )) {
611+ throw new IllegalArgumentException ("Connection URL contains sensitive configuration" );
612+ }
594613
595- if (containsIgnoreCase (decodedUrl , ALLOW_LOAD_LOCAL_IN_FILE_NAME ) ||
596- containsIgnoreCase (decodedUrl , AUTO_DESERIALIZE ) ||
597- containsIgnoreCase (decodedUrl , ALLOW_LOCAL_IN_FILE_NAME ) ||
598- containsIgnoreCase (decodedUrl , ALLOW_URL_IN_LOCAL_IN_FILE_NAME )) {
614+ // the INIT parameter name is a bit generic so we check it only for H2
615+ if (containsIgnoreCase (decodedUrl , "jdbc:h2" ) && containsKeyIgnoreCase (params , INIT )) {
599616 throw new IllegalArgumentException ("Connection URL contains sensitive configuration" );
600617 }
601618 }
602619
620+ /**
621+ * Decode the URL encoded string recursively until no more decoding is needed.
622+ * This is to handle cases where the URL might be double-encoded.
623+ *
624+ * @param url the original URL (for logging purposes)
625+ * @param encoded the URL encoded string
626+ * @param recurseCount the current recursion depth
627+ * @return the decoded string
628+ * @throws IllegalArgumentException if the recursion depth exceeds 10
629+ */
630+ private static String urlDecode (final String url ,
631+ final String encoded ,
632+ final int recurseCount ) {
633+ if (recurseCount > 10 ) {
634+ throw new IllegalArgumentException ("illegal URL encoding detected: " + url );
635+ }
636+ final String decoded = URLDecoder .decode (encoded , StandardCharsets .UTF_8 );
637+ if (decoded .equals (encoded )) {
638+ return decoded ; // No more decoding needed or max recursion reached
639+ }
640+ return urlDecode (url , decoded , recurseCount + 1 );
641+ }
642+
643+ private static Map <String , String > parseUrlParameters (final String url ) {
644+ final Map <String , String > parameters = new HashMap <>();
645+
646+ // MySQL supports parentheses in the URL
647+ // https://dev.mysql.com/doc/connectors/en/connector-j-reference-jdbc-url-format.html
648+ // eg jdbc:mysql://(host=myhost,port=1111,allowLoadLocalInfile=true)/db
649+ int parensIndex = extractFromParens (url , 0 , parameters );
650+ while (parensIndex > 0 ) {
651+ parensIndex = extractFromParens (url , parensIndex , parameters );
652+ }
653+
654+ // Split the URL into the base part and the parameters part
655+ String [] parts = url .split ("[?&;]" );
656+ if (parts .length > 1 ) {
657+ // The first part is the base URL, so we start from the second part
658+ for (int i = 1 ; i < parts .length ; i ++) {
659+ splitNameValue (parts [i ], parameters , true );
660+ }
661+ }
662+ return parameters ;
663+ }
664+
665+ private static boolean containsKeyIgnoreCase (Map <String , String > map , String key ) {
666+ for (String k : map .keySet ()) {
667+ if (k .equalsIgnoreCase (key )) {
668+ return true ;
669+ }
670+ }
671+ return false ;
672+ }
673+
674+ /**
675+ * Extracts key-value pairs from parentheses in the input string.
676+ * The expected format is "(key1=value1, key2=value2, ...)".
677+ *
678+ * @param input the input string containing parameters in parentheses
679+ * @param initIndex the index to start searching for parentheses
680+ * @param parameters the map to store extracted key-value pairs
681+ * @return the index of the closing parenthesis or -1 if not found
682+ */
683+ private static int extractFromParens (final String input ,
684+ final int initIndex ,
685+ final Map <String , String > parameters ) {
686+ final int startIndex = input .indexOf ('(' , initIndex );
687+ if (startIndex == -1 ) {
688+ return -1 ;
689+ }
690+ final int endIndex = input .indexOf (')' , startIndex );
691+ if (startIndex != -1 && endIndex != -1 ) {
692+ String params = input .substring (startIndex + 1 , endIndex );
693+ String [] keyValuePairs = params .split ("," );
694+ for (String pair : keyValuePairs ) {
695+ splitNameValue (pair , parameters , false );
696+ }
697+ }
698+ return endIndex ;
699+ }
700+
701+ /**
702+ * Splits a name-value pair and adds it to the parameters map.
703+ * Handles cases where the value might be missing.
704+ *
705+ * @param nameValue the name-value pair as a string
706+ * @param parameters the map to store the extracted key-value pair
707+ * @param allowEmptyValue whether to allow empty values
708+ */
709+ private static void splitNameValue (String nameValue , Map <String , String > parameters ,
710+ boolean allowEmptyValue ) {
711+ String [] keyValue = nameValue .split ("=" );
712+ if (keyValue .length >= 2 ) {
713+ parameters .put (keyValue [0 ].trim (), keyValue [1 ].trim ());
714+ } else if (allowEmptyValue ) {
715+ // Handle cases where there might not be a value
716+ parameters .put (keyValue [0 ].trim (), "" );
717+ }
718+ }
719+
603720 private String appendProxyUserToURL (String url , String user ) {
604721 StringBuilder connectionUrl = new StringBuilder (url );
605722
0 commit comments