diff --git a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPList.java b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPList.java index 8da71cf3e..0d1fde978 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPList.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPList.java @@ -18,13 +18,20 @@ public void add(String ipOrCIDR) { return; // Don't add if IP is null } IPAddress ip = new IPAddressString(ipOrCIDR).getAddress(); + if (ip == null) { + return; + } + // Normalize IPv4-mapped IPv6 addresses to their IPv4 form so matching is symmetric. + if (ip.isIPv6() && ip.toIPv6().isIPv4Convertible()) { + IPAddress ipv4 = ip.toIPv6().toIPv4(); + if (ipv4 != null) { + ip = ipv4; + } + } if (ipOrCIDR.contains("/")) { - // CIDR : ip = ip.toPrefixBlock(); } - if (ip != null) { - ipAddresses.add(ip); - } + ipAddresses.add(ip); } public boolean matches(String ip) { @@ -34,7 +41,21 @@ public boolean matches(String ip) { } IPAddress ipAddress = ipAddressString.getAddress(); - // Check if the IP address is in any of the blocked subnets + if (containsAddress(ipAddress)) { + return true; + } + + // Also try the embedded IPv4 form for IPv4-mapped IPv6 addresses (e.g. ::ffff:23.45.67.89) + if (ipAddress.isIPv6() && ipAddress.toIPv6().isIPv4Convertible()) { + IPAddress ipv4 = ipAddress.toIPv6().toIPv4(); + if (ipv4 != null && containsAddress(ipv4)) { + return true; + } + } + return false; + } + + private boolean containsAddress(IPAddress ipAddress) { for (IPAddress subnet : ipAddresses) { if (subnet.contains(ipAddress)) { return true; diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/IsPrivateIP.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/IsPrivateIP.java index a494ade5c..c3141b434 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/IsPrivateIP.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/IsPrivateIP.java @@ -1,12 +1,8 @@ package dev.aikido.agent_api.vulnerabilities.ssrf; import dev.aikido.agent_api.helpers.net.IPList; -import inet.ipaddr.IPAddressString; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; public final class IsPrivateIP { // Define private IP ranges @@ -45,10 +41,6 @@ public final class IsPrivateIP { static { PRIVATE_IP_RANGES.stream().forEach(privateIpNetworks::add); PRIVATE_IPV6_RANGES.stream().forEach(privateIpNetworks::add); - // Add IPv4-mapped IPv6 addresses - for (String ipv4Ranges: PRIVATE_IP_RANGES) { - privateIpNetworks.add(mapIPv4ToIPv6(ipv4Ranges)); - } } private IsPrivateIP() { @@ -66,21 +58,4 @@ public static boolean containsPrivateIP(List ipAddresses) { public static boolean isPrivateIp(String ip) { return privateIpNetworks.matches(ip); } - - /** - * Maps an IPv4 address to an IPv6 address. - * e.g. 127.0.0.0/8 -> ::ffff:127.0.0.0/104 - */ - public static String mapIPv4ToIPv6(String ip) { - if (!ip.contains("/")) { - // No CIDR suffix, assume /32 - return "::ffff:" + ip + "/128"; - } - - String[] parts = ip.split("/"); - int suffix = Integer.parseInt(parts[1]); - // We add 96 to the suffix, since ::ffff: already is 96 bits, - // so the 32 remaining bits are decided by the IPv4 address - return "::ffff:" + parts[0] + "/" + (suffix + 96); - } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/IMDSAddresses.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/IMDSAddresses.java index 225a6e7c3..f89bb82a6 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/IMDSAddresses.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/IMDSAddresses.java @@ -1,7 +1,6 @@ package dev.aikido.agent_api.vulnerabilities.ssrf.imds; import dev.aikido.agent_api.helpers.net.IPList; -import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.mapIPv4ToIPv6; public final class IMDSAddresses { private IMDSAddresses() {} @@ -11,11 +10,9 @@ private IMDSAddresses() {} // Add the IP addresses used by AWS EC2 instances for IMDS imdsAddresses.add("169.254.169.254"); imdsAddresses.add("fd00:ec2::254"); - imdsAddresses.add(mapIPv4ToIPv6("169.254.169.254")); // Add the IP addresses used for Alibaba Cloud imdsAddresses.add("100.100.100.200"); - imdsAddresses.add(mapIPv4ToIPv6("100.100.100.200")); } /** Checks if the IP is an IMDS IP */ diff --git a/agent_api/src/test/java/collectors/WebRequestCollectorTest.java b/agent_api/src/test/java/collectors/WebRequestCollectorTest.java index 6c0c548e0..f54f06c71 100644 --- a/agent_api/src/test/java/collectors/WebRequestCollectorTest.java +++ b/agent_api/src/test/java/collectors/WebRequestCollectorTest.java @@ -240,6 +240,26 @@ void testReport_ipBlockedUsingLists_Ip_Bypassed() { assertNull(Context.get()); } + @Test + void testReport_ipBlockedUsingLists_IPv4MappedBypass() { + contextObject.setIp("::ffff:192.168.1.1"); + + ReportingApi.APIListsResponse blockedListsRes = new ReportingApi.APIListsResponse(List.of( + new ReportingApi.ListsResponseEntry("key", "geoip", "geoip restrictions", List.of("192.168.1.1")) + ), List.of(), List.of(), null, null, List.of()); + ServiceConfigStore.updateFromAPIListsResponse(blockedListsRes); + + List bypassedIps = List.of("192.168.1.1"); + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, false, null, true, false, List.of() + )); + + WebRequestCollector.Res response = WebRequestCollector.report(contextObject); + + assertNull(response); + assertNull(Context.get()); + } + @Test void testReport_ipNotAllowedUsingLists_Ip_Bypassed() { ReportingApi.APIListsResponse blockedListsRes = new ReportingApi.APIListsResponse( diff --git a/agent_api/src/test/java/helpers/IPListTest.java b/agent_api/src/test/java/helpers/net/IPListTest.java similarity index 69% rename from agent_api/src/test/java/helpers/IPListTest.java rename to agent_api/src/test/java/helpers/net/IPListTest.java index ced9f3388..6117bb648 100644 --- a/agent_api/src/test/java/helpers/IPListTest.java +++ b/agent_api/src/test/java/helpers/net/IPListTest.java @@ -1,4 +1,4 @@ -package helpers; +package helpers.net; import dev.aikido.agent_api.helpers.net.IPList; import org.junit.jupiter.api.BeforeEach; @@ -114,4 +114,58 @@ public void testBlocklistSubnetWithSubnet2() { assertTrue(blocklist.matches("192.168.2.1")); assertTrue(blocklist.matches("192.168.2.2")); } + + @Test + public void testBlocklistMatchesIPv4MappedIPv6() { + blocklist.add("192.168.1.1"); + assertTrue(blocklist.matches("::ffff:192.168.1.1")); + assertFalse(blocklist.matches("::ffff:192.168.1.2")); + + blocklist.add("10.0.0.0/8"); + assertTrue(blocklist.matches("::ffff:10.5.6.7")); + assertTrue(blocklist.matches("::ffff:10.0.0.1")); + assertFalse(blocklist.matches("::ffff:11.0.0.1")); + } + + @Test + public void testBlocklistIPv6OnlyIgnoresIPv4MappedMismatch() { + blocklist.add("2001:db8::/32"); + assertTrue(blocklist.matches("2001:db8::1")); + assertFalse(blocklist.matches("::ffff:192.168.1.1")); + assertFalse(blocklist.matches("192.168.1.1")); + } + + @Test + public void testBlocklistStoredIPv4MappedMatchesIPv4Input() { + blocklist.add("::ffff:23.45.67.89"); + assertTrue(blocklist.matches("::ffff:23.45.67.89")); + } + + @Test + public void testBlocklistAddInvalidIpIgnored() { + blocklist.add("notanip"); + assertEquals(0, blocklist.length()); + assertFalse(blocklist.matches("192.168.1.1")); + } + + @Test + public void testBlocklistLengthEmpty() { + assertEquals(0, blocklist.length()); + } + + @Test + public void testBlocklistStoredIPv4MappedMatchesPlainIPv4() { + blocklist.add("::ffff:23.45.67.89"); + assertTrue(blocklist.matches("23.45.67.89")); + assertTrue(blocklist.matches("::ffff:23.45.67.89")); + assertFalse(blocklist.matches("23.45.67.90")); + } + + @Test + public void testBlocklistStoredIPv4MappedCidrMatchesPlainIPv4() { + blocklist.add("::ffff:10.0.0.0/104"); + assertTrue(blocklist.matches("10.1.2.3")); + assertTrue(blocklist.matches("::ffff:10.1.2.3")); + assertFalse(blocklist.matches("11.1.2.3")); + } } diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java index f66002588..7eefb38c8 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java @@ -3,23 +3,10 @@ import org.junit.jupiter.api.Test; import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.isPrivateIp; -import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.mapIPv4ToIPv6; import static org.junit.jupiter.api.Assertions.*; public class IsPrivateIPTest { - @Test - public void testMapIPv4ToIPv6() { - assertEquals("::ffff:127.0.0.0/128", mapIPv4ToIPv6("127.0.0.0")); - assertEquals("::ffff:127.0.0.0/104", mapIPv4ToIPv6("127.0.0.0/8")); - assertEquals("::ffff:10.0.0.0/128", mapIPv4ToIPv6("10.0.0.0")); - assertEquals("::ffff:10.0.0.0/104", mapIPv4ToIPv6("10.0.0.0/8")); - assertEquals("::ffff:10.0.0.1/128", mapIPv4ToIPv6("10.0.0.1")); - assertEquals("::ffff:10.0.0.1/104", mapIPv4ToIPv6("10.0.0.1/8")); - assertEquals("::ffff:192.168.0.0/112", mapIPv4ToIPv6("192.168.0.0/16")); - assertEquals("::ffff:172.16.0.0/108", mapIPv4ToIPv6("172.16.0.0/12")); - } - @Test void testPrivateIPv4Addresses() { assertTrue(isPrivateIp("0.0.0.0"));