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 e90c58a418..0f4312be98 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,18 @@ 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 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()); - - 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 configValue = entry.getValue(); + if (configValue != null && !configValue.trim().isEmpty()) { + 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 +455,100 @@ Long parseLong(String str) { } } - private static URL getValidURL(String urlString) { + private static String extractHost(String urlString) { + try { + String processedUrl = urlString.trim().replaceFirst("(?i)^jdbc:", ""); + if (!processedUrl.contains("://")) { + processedUrl = "http://" + processedUrl; + } + + processedUrl = processedUrl.replaceFirst("://([0-9a-fA-F:]+):(\\d+)(/.*)?$", "://[$1]:$2$3"); + + URI uri = new URI(processedUrl); + String host = uri.getHost(); + + if (host == null && uri.getAuthority() != null) { + host = uri.getAuthority(); + // Strip credentials if present (e.g., user:pass@127.1) + 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 { - return new URL(urlString); + 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 { + 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 30de32adca..3536e59ec4 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 @@ -183,14 +183,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(); @@ -289,4 +281,66 @@ public void test10_lookupResource_tagServiceDirectCall() throws Exception { mgr.lookupResource("s", ctx, store); Assertions.assertTrue(true); } + + @Test + void test11_validateConfig_rejectsSSRFBypassAttempts() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + TimedExecutor exec = mock(TimedExecutor.class); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); // Correct field name + setField(mgr, ServiceMgr.class, "timedExecutor", exec); + + Map bypassConfigs = new HashMap<>(); + bypassConfigs.put("jdbc.url", "jdbc:mysql://127.1:3306/db"); // Shorthand IP + bypassConfigs.put("api.endpoint", "http://0x7f000001:8080"); // Hexadecimal IP + bypassConfigs.put("redis.host", "2130706433:6379"); // Decimal IP + bypassConfigs.put("service.url", "http://::1:8080"); // IPv6 localhost + + RangerService svc = new RangerService(); + svc.setType("hive"); + svc.setName("test-service"); + svc.setConfigs(bypassConfigs); + + ServiceStore store = mock(ServiceStore.class); // Correct type + 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); + + try { + mgr.validateConfig(svc, store); + Assertions.fail("Expected exception for SSRF bypass attempt"); + } catch (Exception expected) { + Assertions.assertTrue(expected.getMessage().contains("is not allowed") || expected.getMessage().contains("blocked host detected")); + } + } + + @Test + void test12_validateConfig_allowsLegitimateExternalUrls() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + + Map 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); + + try { + mgr.validateConfig(svc, null); + } catch (Exception e) { + Assertions.assertFalse(e.getMessage().contains("is not allowed"), "SSRF validation incorrectly blocked legitimate URL: " + e.getMessage()); + Assertions.assertFalse(e.getMessage().contains("blocked host detected"), "SSRF validation incorrectly blocked legitimate URL: " + e.getMessage()); + } + + Assertions.assertTrue(true, "SSRF validation correctly allowed legitimate URLs"); + } }