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 8bd65753a RANGER-5541: Implement best coding practices for validating
service configs (#916)
8bd65753a is described below
commit 8bd65753ac91a91e3552a93f73c8cad5d39491c6
Author: Sanket-Shelar <[email protected]>
AuthorDate: Tue Sep 29 16:27:28 2026 +0530
RANGER-5541: Implement best coding practices for validating service configs
(#916)
---
.../java/org/apache/ranger/biz/ServiceMgr.java | 147 ++++++++++++++++++++-
.../java/org/apache/ranger/biz/TestServiceMgr.java | 74 +++++++++--
2 files changed, 207 insertions(+), 14 deletions(-)
diff --git a/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
b/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
index c5f929cb9..515dd98d9 100755
--- a/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
+++ b/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
@@ -48,8 +48,11 @@
import org.springframework.stereotype.Component;
import java.io.File;
+import java.net.InetAddress;
+import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -204,11 +207,23 @@ public VXResponse validateConfig(RangerService service,
ServiceStore svcStore) t
// check if service configs contains localhost/127.0.0.1
if (service != null && service.getConfigs() != null) {
for (Map.Entry<String, String> entry :
service.getConfigs().entrySet()) {
- if (entry.getValue() != null &&
StringUtils.containsIgnoreCase(entry.getValue(), "localhost") ||
StringUtils.containsIgnoreCase(entry.getValue(), "127.0.0.1")) {
- URL url = getValidURL(entry.getValue());
+ String configValue = entry.getValue();
+ if (configValue != null && !configValue.trim().isEmpty()) {
+ String userInfo = extractUserInfo(configValue);
+ if (userInfoContainsBlockedHost(userInfo)) {
+ throw new Exception("Invalid value for configuration "
+ entry.getKey() + ": userinfo contains blocked host (blocked host detected)");
+ }
- if ((url != null) &&
(url.getHost().equalsIgnoreCase("localhost") ||
url.getHost().equals("127.0.0.1"))) {
- throw new Exception("Invalid value for configuration "
+ entry.getKey() + ": host " + url.getHost() + " is not allowed");
+ String host = extractHost(configValue);
+ if (host != null) {
+ if (isBlockedHost(host)) {
+ throw new Exception("Invalid value for
configuration " + entry.getKey() + ": host " + host + " is not allowed (blocked
host detected)");
+ }
+ } else {
+ String lowerVal = configValue.toLowerCase().trim();
+ if (lowerVal.contains("localhost") ||
lowerVal.contains("127.0.0.1") || lowerVal.contains("0.0.0.0") ||
lowerVal.contains("::1")) {
+ throw new Exception("Invalid value for
configuration " + entry.getKey() + ": contains blocked keywords but could not
be parsed securely.");
+ }
}
}
}
@@ -445,9 +460,129 @@ Long parseLong(String str) {
}
}
- private static URL getValidURL(String urlString) {
+ private static URI toConfigUri(String urlString) throws Exception {
+ String processedUrl = urlString.trim().replaceFirst("(?i)^jdbc:", "");
+ if (!processedUrl.contains("://")) {
+ processedUrl = "http://" + processedUrl;
+ }
+
+ // Bracket IPv6 literals only (must contain ':' in the host segment;
avoids decimal IPs like 2130706433:6379)
+ processedUrl =
processedUrl.replaceFirst("://((?:[0-9a-fA-F]*:)+[0-9a-fA-F]*):(\\d+)(/.*)?$",
"://[$1]:$2$3");
+
+ return new URI(processedUrl);
+ }
+
+ private static String extractUserInfo(String urlString) {
+ try {
+ return toConfigUri(urlString).getUserInfo();
+ } catch (Exception e) {
+ LOG.debug("Failed to extract userinfo from string: {}", urlString,
e);
+ return null;
+ }
+ }
+
+ private static boolean userInfoContainsBlockedHost(String userInfo) {
+ if (StringUtils.isBlank(userInfo)) {
+ return false;
+ }
+
+ String[] segments = userInfo.split(":");
+ for (String segment : segments) {
+ if (StringUtils.isNotBlank(segment) &&
isBlockedHost(segment.trim())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static String extractHost(String urlString) {
+ try {
+ URI uri = toConfigUri(urlString);
+ String host = uri.getHost();
+
+ if (host == null && uri.getAuthority() != null) {
+ host = uri.getAuthority();
+ // Strip credentials if present (e.g., user:[email protected])
+ if (host.contains("@")) {
+ host = host.substring(host.lastIndexOf("@") + 1);
+ }
+
+ if (host.startsWith("[")) {
+ int closeBracket = host.indexOf("]");
+ int portColon = host.indexOf(":", closeBracket);
+ if (portColon > -1) {
+ host = host.substring(0, portColon);
+ }
+ } else if (host.contains(":")) {
+ host = host.substring(0, host.indexOf(":"));
+ }
+ }
+
+ return host != null ? host.replaceAll("^\\[|\\]$", "") : null;
+ } catch (Exception e) {
+ LOG.debug("Failed to extract host from string: {}", urlString, e);
+ return null;
+ }
+ }
+
+ private static boolean isBlockedHost(String host) {
+ if (host == null || host.trim().isEmpty()) {
+ return false;
+ }
+ host = host.trim().toLowerCase();
+
+ // Expand POSIX shorthand IPs (e.g., 127.1 -> 127.0.0.1)
+ String normalizedIP = normalizePOSIXIp(host);
+ if (normalizedIP != null) {
+ host = normalizedIP;
+ }
+
+ try {
+ InetAddress addr = InetAddress.getByName(host);
+
+ return addr.isLoopbackAddress() || addr.isLinkLocalAddress() ||
addr.isAnyLocalAddress() || "localhost".equals(host);
+ } catch (UnknownHostException e) {
+ LOG.debug("Host could not be resolved, allowing by default: {}",
host);
+ return false;
+ }
+ }
+
+ private static String normalizePOSIXIp(String host) {
+ String[] parts = host.split("\\.");
+ if (parts.length < 1 || parts.length > 4) {
+ return null;
+ }
+
try {
- return new URL(urlString);
+ long[] vals = new long[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ String p = parts[i].trim();
+ if (p.isEmpty()) {
+ return null;
+ }
+
+ int radix = (p.startsWith("0x") || p.startsWith("0X")) ? 16 :
(p.startsWith("0") && p.length() > 1 ? 8 : 10);
+ vals[i] = Long.parseLong(p.replaceFirst("(?i)^0x", ""), radix);
+ }
+
+ long ip = 0;
+ switch (parts.length) {
+ case 1:
+ ip = vals[0];
+ break;
+ case 2:
+ ip = (vals[0] << 24) | vals[1];
+ break;
+ case 3:
+ ip = (vals[0] << 24) | (vals[1] << 16) | vals[2];
+ break;
+ case 4:
+ ip = (vals[0] << 24) | (vals[1] << 16) | (vals[2] << 8) |
vals[3];
+ break;
+ }
+
+ return String.format("%d.%d.%d.%d", (ip >> 24) & 255, (ip >> 16) &
255, (ip >> 8) & 255, ip & 255);
} catch (Exception e) {
return null;
}
diff --git
a/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
b/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
index 362b0cbab..ecf16c510 100644
--- a/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
+++ b/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
@@ -35,6 +35,9 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
@@ -47,6 +50,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -183,14 +187,6 @@ public void test05_isZoneAdmin_and_isZoneAuditor() throws
Exception {
Assertions.assertTrue(mgr.isZoneAuditor("z1"));
}
- @Test
- public void test06_getValidURL_validAndInvalid() throws Exception {
- Method m = ServiceMgr.class.getDeclaredMethod("getValidURL",
String.class);
- m.setAccessible(true);
- Assertions.assertNotNull(m.invoke(null, "http://example.com"));
- Assertions.assertNull(m.invoke(null, "not_a_url"));
- }
-
@Test
public void test07_getFilesInDirectory_existingAndMissing() throws
Exception {
ServiceMgr mgr = new ServiceMgr();
@@ -327,4 +323,66 @@ public void
test12_getRangerServiceByService_acceptsImplClassAssignableToRangerB
Assertions.assertNotNull(built);
Assertions.assertEquals(RangerDefaultService.class, built.getClass());
}
+
+ static Stream<Arguments> ssrfBypassConfigValues() {
+ return Stream.of(
+ Arguments.of("jdbc.url", "jdbc:mysql://127.1:3306/db"),
+ Arguments.of("api.endpoint", "http://0x7f000001:8080"),
+ Arguments.of("redis.host", "2130706433:6379"),
+ Arguments.of("service.url", "http://::1:8080"),
+ Arguments.of("api.endpoint",
"http://[email protected]:8080"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("ssrfBypassConfigValues")
+ public void test11_validateConfig_rejectsSSRFBypassAttempts(String
configKey, String configValue) throws Exception {
+ ServiceMgr mgr = new ServiceMgr();
+ RangerServiceService svcService = mock(RangerServiceService.class);
+ TimedExecutor exec = mock(TimedExecutor.class);
+ setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+ setField(mgr, ServiceMgr.class, "timedExecutor", exec);
+
+ Map<String, String> bypassConfigs = new HashMap<>();
+ bypassConfigs.put(configKey, configValue);
+
+ RangerService svc = new RangerService();
+ svc.setType("hive");
+ svc.setName("test-service");
+ svc.setConfigs(bypassConfigs);
+
+ ServiceStore store = mock(ServiceStore.class);
+ RangerServiceDef def = new RangerServiceDef();
+ def.setName("hive");
+ def.setImplClass(RangerDefaultService.class.getName());
+ when(store.getServiceDefByName("hive")).thenReturn(def);
+
when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(bypassConfigs);
+
+ Exception thrown = Assertions.assertThrows(Exception.class, () ->
mgr.validateConfig(svc, store));
+ Assertions.assertTrue(
+ thrown.getMessage().contains("is not allowed") ||
thrown.getMessage().contains("blocked host detected"),
+ "Unexpected message: " + thrown.getMessage());
+ }
+
+ @Test
+ public void test13_validateConfig_allowsLegitimateExternalUrls() throws
Exception {
+ ServiceMgr mgr = new ServiceMgr();
+ RangerServiceService svcService = mock(RangerServiceService.class);
+ setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+
+ Map<String, String> legitimateConfigs = new HashMap<>();
+ legitimateConfigs.put("jdbc.url",
"jdbc:mysql://db.company.com:3306/prod");
+ legitimateConfigs.put("api.endpoint", "https://api.github.com:443");
+ legitimateConfigs.put("user.list", "hive,impala,trino"); // Non-URL
config
+
+ RangerService svc = new RangerService();
+ svc.setType("hive");
+ svc.setName("test-service");
+ svc.setConfigs(legitimateConfigs);
+
+
when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(legitimateConfigs);
+
+ VXResponse response = mgr.validateConfig(svc, null);
+ Assertions.assertNotNull(response);
+ Assertions.assertNotEquals(VXResponse.STATUS_ERROR,
response.getStatusCode());
+ }
}