Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -66,21 +58,4 @@ public static boolean containsPrivateIP(List<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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() {}
Expand All @@ -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 */
Expand Down
20 changes: 20 additions & 0 deletions agent_api/src/test/java/collectors/WebRequestCollectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package helpers;
package helpers.net;

import dev.aikido.agent_api.helpers.net.IPList;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -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"));
}
}
13 changes: 0 additions & 13 deletions agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Loading