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());
+    }
 }

Reply via email to