Skip to content

Commit a4f73ee

Browse files
Merge pull request #282 from AikidoSec/fix-ipv4-mapped-ipv6-for-bypassed-ips
IPList: Check for ipv4-mapped ipv6 addresses
2 parents df2cf03 + ba3c6e2 commit a4f73ee

6 files changed

Lines changed: 101 additions & 47 deletions

File tree

agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPList.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ public void add(String ipOrCIDR) {
1818
return; // Don't add if IP is null
1919
}
2020
IPAddress ip = new IPAddressString(ipOrCIDR).getAddress();
21+
if (ip == null) {
22+
return;
23+
}
24+
// Normalize IPv4-mapped IPv6 addresses to their IPv4 form so matching is symmetric.
25+
if (ip.isIPv6() && ip.toIPv6().isIPv4Convertible()) {
26+
IPAddress ipv4 = ip.toIPv6().toIPv4();
27+
if (ipv4 != null) {
28+
ip = ipv4;
29+
}
30+
}
2131
if (ipOrCIDR.contains("/")) {
22-
// CIDR :
2332
ip = ip.toPrefixBlock();
2433
}
25-
if (ip != null) {
26-
ipAddresses.add(ip);
27-
}
34+
ipAddresses.add(ip);
2835
}
2936

3037
public boolean matches(String ip) {
@@ -34,7 +41,21 @@ public boolean matches(String ip) {
3441
}
3542
IPAddress ipAddress = ipAddressString.getAddress();
3643

37-
// Check if the IP address is in any of the blocked subnets
44+
if (containsAddress(ipAddress)) {
45+
return true;
46+
}
47+
48+
// Also try the embedded IPv4 form for IPv4-mapped IPv6 addresses (e.g. ::ffff:23.45.67.89)
49+
if (ipAddress.isIPv6() && ipAddress.toIPv6().isIPv4Convertible()) {
50+
IPAddress ipv4 = ipAddress.toIPv6().toIPv4();
51+
if (ipv4 != null && containsAddress(ipv4)) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
58+
private boolean containsAddress(IPAddress ipAddress) {
3859
for (IPAddress subnet : ipAddresses) {
3960
if (subnet.contains(ipAddress)) {
4061
return true;

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/IsPrivateIP.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package dev.aikido.agent_api.vulnerabilities.ssrf;
22

33
import dev.aikido.agent_api.helpers.net.IPList;
4-
import inet.ipaddr.IPAddressString;
54

6-
import java.util.Arrays;
7-
import java.util.HashSet;
85
import java.util.List;
9-
import java.util.Set;
106

117
public final class IsPrivateIP {
128
// Define private IP ranges
@@ -45,10 +41,6 @@ public final class IsPrivateIP {
4541
static {
4642
PRIVATE_IP_RANGES.stream().forEach(privateIpNetworks::add);
4743
PRIVATE_IPV6_RANGES.stream().forEach(privateIpNetworks::add);
48-
// Add IPv4-mapped IPv6 addresses
49-
for (String ipv4Ranges: PRIVATE_IP_RANGES) {
50-
privateIpNetworks.add(mapIPv4ToIPv6(ipv4Ranges));
51-
}
5244
}
5345

5446
private IsPrivateIP() {
@@ -66,21 +58,4 @@ public static boolean containsPrivateIP(List<String> ipAddresses) {
6658
public static boolean isPrivateIp(String ip) {
6759
return privateIpNetworks.matches(ip);
6860
}
69-
70-
/**
71-
* Maps an IPv4 address to an IPv6 address.
72-
* e.g. 127.0.0.0/8 -> ::ffff:127.0.0.0/104
73-
*/
74-
public static String mapIPv4ToIPv6(String ip) {
75-
if (!ip.contains("/")) {
76-
// No CIDR suffix, assume /32
77-
return "::ffff:" + ip + "/128";
78-
}
79-
80-
String[] parts = ip.split("/");
81-
int suffix = Integer.parseInt(parts[1]);
82-
// We add 96 to the suffix, since ::ffff: already is 96 bits,
83-
// so the 32 remaining bits are decided by the IPv4 address
84-
return "::ffff:" + parts[0] + "/" + (suffix + 96);
85-
}
8661
}

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/IMDSAddresses.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.aikido.agent_api.vulnerabilities.ssrf.imds;
22

33
import dev.aikido.agent_api.helpers.net.IPList;
4-
import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.mapIPv4ToIPv6;
54

65
public final class IMDSAddresses {
76
private IMDSAddresses() {}
@@ -11,11 +10,9 @@ private IMDSAddresses() {}
1110
// Add the IP addresses used by AWS EC2 instances for IMDS
1211
imdsAddresses.add("169.254.169.254");
1312
imdsAddresses.add("fd00:ec2::254");
14-
imdsAddresses.add(mapIPv4ToIPv6("169.254.169.254"));
1513

1614
// Add the IP addresses used for Alibaba Cloud
1715
imdsAddresses.add("100.100.100.200");
18-
imdsAddresses.add(mapIPv4ToIPv6("100.100.100.200"));
1916
}
2017

2118
/** Checks if the IP is an IMDS IP */

agent_api/src/test/java/collectors/WebRequestCollectorTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,26 @@ void testReport_ipBlockedUsingLists_Ip_Bypassed() {
240240
assertNull(Context.get());
241241
}
242242

243+
@Test
244+
void testReport_ipBlockedUsingLists_IPv4MappedBypass() {
245+
contextObject.setIp("::ffff:192.168.1.1");
246+
247+
ReportingApi.APIListsResponse blockedListsRes = new ReportingApi.APIListsResponse(List.of(
248+
new ReportingApi.ListsResponseEntry("key", "geoip", "geoip restrictions", List.of("192.168.1.1"))
249+
), List.of(), List.of(), null, null, List.of());
250+
ServiceConfigStore.updateFromAPIListsResponse(blockedListsRes);
251+
252+
List<String> bypassedIps = List.of("192.168.1.1");
253+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
254+
true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, false, null, true, false, List.of()
255+
));
256+
257+
WebRequestCollector.Res response = WebRequestCollector.report(contextObject);
258+
259+
assertNull(response);
260+
assertNull(Context.get());
261+
}
262+
243263
@Test
244264
void testReport_ipNotAllowedUsingLists_Ip_Bypassed() {
245265
ReportingApi.APIListsResponse blockedListsRes = new ReportingApi.APIListsResponse(

agent_api/src/test/java/helpers/IPListTest.java renamed to agent_api/src/test/java/helpers/net/IPListTest.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package helpers;
1+
package helpers.net;
22

33
import dev.aikido.agent_api.helpers.net.IPList;
44
import org.junit.jupiter.api.BeforeEach;
@@ -114,4 +114,58 @@ public void testBlocklistSubnetWithSubnet2() {
114114
assertTrue(blocklist.matches("192.168.2.1"));
115115
assertTrue(blocklist.matches("192.168.2.2"));
116116
}
117+
118+
@Test
119+
public void testBlocklistMatchesIPv4MappedIPv6() {
120+
blocklist.add("192.168.1.1");
121+
assertTrue(blocklist.matches("::ffff:192.168.1.1"));
122+
assertFalse(blocklist.matches("::ffff:192.168.1.2"));
123+
124+
blocklist.add("10.0.0.0/8");
125+
assertTrue(blocklist.matches("::ffff:10.5.6.7"));
126+
assertTrue(blocklist.matches("::ffff:10.0.0.1"));
127+
assertFalse(blocklist.matches("::ffff:11.0.0.1"));
128+
}
129+
130+
@Test
131+
public void testBlocklistIPv6OnlyIgnoresIPv4MappedMismatch() {
132+
blocklist.add("2001:db8::/32");
133+
assertTrue(blocklist.matches("2001:db8::1"));
134+
assertFalse(blocklist.matches("::ffff:192.168.1.1"));
135+
assertFalse(blocklist.matches("192.168.1.1"));
136+
}
137+
138+
@Test
139+
public void testBlocklistStoredIPv4MappedMatchesIPv4Input() {
140+
blocklist.add("::ffff:23.45.67.89");
141+
assertTrue(blocklist.matches("::ffff:23.45.67.89"));
142+
}
143+
144+
@Test
145+
public void testBlocklistAddInvalidIpIgnored() {
146+
blocklist.add("notanip");
147+
assertEquals(0, blocklist.length());
148+
assertFalse(blocklist.matches("192.168.1.1"));
149+
}
150+
151+
@Test
152+
public void testBlocklistLengthEmpty() {
153+
assertEquals(0, blocklist.length());
154+
}
155+
156+
@Test
157+
public void testBlocklistStoredIPv4MappedMatchesPlainIPv4() {
158+
blocklist.add("::ffff:23.45.67.89");
159+
assertTrue(blocklist.matches("23.45.67.89"));
160+
assertTrue(blocklist.matches("::ffff:23.45.67.89"));
161+
assertFalse(blocklist.matches("23.45.67.90"));
162+
}
163+
164+
@Test
165+
public void testBlocklistStoredIPv4MappedCidrMatchesPlainIPv4() {
166+
blocklist.add("::ffff:10.0.0.0/104");
167+
assertTrue(blocklist.matches("10.1.2.3"));
168+
assertTrue(blocklist.matches("::ffff:10.1.2.3"));
169+
assertFalse(blocklist.matches("11.1.2.3"));
170+
}
117171
}

agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,10 @@
33
import org.junit.jupiter.api.Test;
44

55
import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.isPrivateIp;
6-
import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.mapIPv4ToIPv6;
76
import static org.junit.jupiter.api.Assertions.*;
87

98
public class IsPrivateIPTest {
109

11-
@Test
12-
public void testMapIPv4ToIPv6() {
13-
assertEquals("::ffff:127.0.0.0/128", mapIPv4ToIPv6("127.0.0.0"));
14-
assertEquals("::ffff:127.0.0.0/104", mapIPv4ToIPv6("127.0.0.0/8"));
15-
assertEquals("::ffff:10.0.0.0/128", mapIPv4ToIPv6("10.0.0.0"));
16-
assertEquals("::ffff:10.0.0.0/104", mapIPv4ToIPv6("10.0.0.0/8"));
17-
assertEquals("::ffff:10.0.0.1/128", mapIPv4ToIPv6("10.0.0.1"));
18-
assertEquals("::ffff:10.0.0.1/104", mapIPv4ToIPv6("10.0.0.1/8"));
19-
assertEquals("::ffff:192.168.0.0/112", mapIPv4ToIPv6("192.168.0.0/16"));
20-
assertEquals("::ffff:172.16.0.0/108", mapIPv4ToIPv6("172.16.0.0/12"));
21-
}
22-
2310
@Test
2411
void testPrivateIPv4Addresses() {
2512
assertTrue(isPrivateIp("0.0.0.0"));

0 commit comments

Comments
 (0)