diff --git a/agent_api/build.gradle b/agent_api/build.gradle index 88620e512..341c346f6 100644 --- a/agent_api/build.gradle +++ b/agent_api/build.gradle @@ -81,15 +81,11 @@ def jacocoClassDirectories = files(sourceSets.main.output.files.collect { }) dependencies { - implementation 'com.github.seancfoley:ipaddress:5.5.1' implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.dylibso.chicory:runtime:1.7.5' // Junixsocket imports : implementation 'com.kohlschutter.junixsocket:junixsocket-core:2.10.1' implementation 'com.kohlschutter.junixsocket:junixsocket-server:2.10.1' - // Subnets : - implementation 'com.github.seancfoley:ipaddress:5.3.3' - // For middleware : compileOnly 'io.javalin:javalin:6.3.0' diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/Endpoint.java b/agent_api/src/main/java/dev/aikido/agent_api/background/Endpoint.java index 217b804a8..1be4a1777 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/Endpoint.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/Endpoint.java @@ -12,6 +12,7 @@ public record RateLimitingConfig(long maxRequests, long windowSizeInMS, boolean private final String route; private final RateLimitingConfig rateLimiting; private final List allowedIPAddresses; + private transient volatile IPList allowedIPMatcher; private final boolean graphql; private final boolean forceProtectionOff; public Endpoint( @@ -21,6 +22,7 @@ public Endpoint( this.method = method; this.route = route; this.allowedIPAddresses = allowedIPAddresses; + this.allowedIPMatcher = createIPList(allowedIPAddresses); this.rateLimiting = new RateLimitingConfig(maxRequests, windowSizeMS, rateLimitingEnabled); this.graphql = graphql; this.forceProtectionOff = forceProtectionOff; @@ -47,7 +49,14 @@ public boolean allowedIpAddressesEmpty() { return allowedIPAddresses == null || allowedIPAddresses.size() == 0; } + private IPList getOrCreateAllowedIPMatcher() { + if (allowedIPMatcher == null) { + this.allowedIPMatcher = createIPList(allowedIPAddresses); + } + return allowedIPMatcher; + } + public boolean isIpAllowed(String ip) { - return createIPList(allowedIPAddresses).matches(ip); + return getOrCreateAllowedIPMatcher().matches(ip); } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/helpers/IPListBuilder.java b/agent_api/src/main/java/dev/aikido/agent_api/helpers/IPListBuilder.java index 7a4ecc822..611d9dbc4 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/helpers/IPListBuilder.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/helpers/IPListBuilder.java @@ -3,22 +3,11 @@ import dev.aikido.agent_api.helpers.net.IPList; import java.util.Collection; -import java.util.List; -import java.util.Set; public final class IPListBuilder { private IPListBuilder() {} public static IPList createIPList(Collection ips) { - IPList ipList = new IPList(); - if (ips == null) { - return ipList; // Don't iterate over null. - } - for (String ip: ips) { - // Add ip address or subnet to IP list : - ipList.add(ip); - } - - return ipList; + return new IPList(ips); } } 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 01813d516..ab06c798f 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 @@ -1,62 +1,28 @@ package dev.aikido.agent_api.helpers.net; -import inet.ipaddr.IPAddress; -import inet.ipaddr.IPAddressString; -import inet.ipaddr.format.util.DualIPv4v6Tries; +import java.util.Collection; +import java.util.List; public class IPList { - private final DualIPv4v6Tries ipAddresses; + private IPMatcher matcher; public IPList() { - this.ipAddresses = new DualIPv4v6Tries(); + matcher = IPMatcher.from(List.of()); + } + + public IPList(Collection ipAddresses) { + matcher = IPMatcher.from(ipAddresses); } public void add(String ipOrCIDR) { - if (ipOrCIDR == null) { - 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("/")) { - ip = ip.toPrefixBlock(); - } - ipAddresses.add(ip); + matcher = matcher.add(ipOrCIDR); } public boolean matches(String ip) { - IPAddressString ipAddressString = new IPAddressString(ip); - if (!ipAddressString.isValid()) { - return false; // Invalid IP address - } - IPAddress ipAddress = ipAddressString.getAddress(); - - 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; + return matcher.matches(ip); } - private boolean containsAddress(IPAddress ipAddress) { - return ipAddresses.elementContains(ipAddress); - } public int length() { - return ipAddresses.size(); + return matcher.size(); } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPMatcher.java b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPMatcher.java new file mode 100644 index 000000000..c791acf63 --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/IPMatcher.java @@ -0,0 +1,1116 @@ +package dev.aikido.agent_api.helpers.net; + +import java.util.Arrays; +import java.util.Collection; + +// This parser and matcher are based on firewall-node's netparser-derived implementation. +// netparser is MIT licensed, Copyright (c) 2019 alex. +final class IPMatcher { + private static final int IPV4 = 4; + private static final int IPV6 = 16; + private static final int IPV4_BITS = 32; + private static final int IPV6_BITS = 128; + private static final ThreadLocal LOOKUP_NETWORK = + ThreadLocal.withInitial(ParsedNetwork::new); + private static final IPMatcher EMPTY = + new IPMatcher(new int[0], new byte[0], new long[0], new long[0], new byte[0]); + + private final int[] ipv4Addresses; + private final byte[] ipv4Prefixes; + private final long[] ipv6HighAddresses; + private final long[] ipv6LowAddresses; + private final byte[] ipv6Prefixes; + + private IPMatcher( + int[] ipv4Addresses, + byte[] ipv4Prefixes, + long[] ipv6HighAddresses, + long[] ipv6LowAddresses, + byte[] ipv6Prefixes) { + this.ipv4Addresses = ipv4Addresses; + this.ipv4Prefixes = ipv4Prefixes; + this.ipv6HighAddresses = ipv6HighAddresses; + this.ipv6LowAddresses = ipv6LowAddresses; + this.ipv6Prefixes = ipv6Prefixes; + } + + static IPMatcher from(Collection networks) { + if (networks == null || networks.isEmpty()) { + return EMPTY; + } + + IPv4Builder ipv4 = new IPv4Builder(); + IPv6Builder ipv6 = new IPv6Builder(); + ParsedNetwork parsed = new ParsedNetwork(); + for (String network : networks) { + if (!Parser.parseBaseNetwork(network, parsed)) { + continue; + } + addParsedNetwork(ipv4, ipv6, parsed); + } + + IPv4Networks compactIPv4 = ipv4.build(); + IPv6Networks compactIPv6 = ipv6.build(); + if (compactIPv4.addresses.length == 0 && compactIPv6.highAddresses.length == 0) { + return EMPTY; + } + return new IPMatcher( + compactIPv4.addresses, + compactIPv4.prefixes, + compactIPv6.highAddresses, + compactIPv6.lowAddresses, + compactIPv6.prefixes); + } + + IPMatcher add(String network) { + ParsedNetwork parsed = new ParsedNetwork(); + if (!Parser.parseBaseNetwork(network, parsed)) { + return this; + } + normalizeMappedNetwork(parsed); + if (parsed.version == IPV4) { + IPv4Builder builder = new IPv4Builder(); + for (int index = 0; index < ipv4Addresses.length; index++) { + builder.add(ipv4Addresses[index], Byte.toUnsignedInt(ipv4Prefixes[index])); + } + builder.add(parsed.ipv4Address, parsed.prefix); + IPv4Networks compact = builder.build(); + return new IPMatcher( + compact.addresses, compact.prefixes, ipv6HighAddresses, ipv6LowAddresses, ipv6Prefixes); + } + + IPv6Builder builder = new IPv6Builder(); + for (int index = 0; index < ipv6HighAddresses.length; index++) { + builder.add( + ipv6HighAddresses[index], + ipv6LowAddresses[index], + Byte.toUnsignedInt(ipv6Prefixes[index])); + } + builder.add(parsed.ipv6HighAddress, parsed.ipv6LowAddress, parsed.prefix); + IPv6Networks compact = builder.build(); + return new IPMatcher( + ipv4Addresses, ipv4Prefixes, compact.highAddresses, compact.lowAddresses, compact.prefixes); + } + + boolean matches(String value) { + if (value == null) { + return false; + } + + ParsedNetwork parsed = LOOKUP_NETWORK.get(); + if (Parser.parseStrictIPv4Address(value, parsed)) { + return containsIPv4(parsed.ipv4Address, IPV4_BITS); + } + if (!Parser.parseBaseNetwork(value, parsed)) { + return false; + } + if (parsed.version == IPV4) { + return containsIPv4(parsed.ipv4Address, parsed.prefix); + } + if (containsIPv6(parsed.ipv6HighAddress, parsed.ipv6LowAddress, parsed.prefix)) { + return true; + } + if (isIPv4Mapped(parsed.ipv6HighAddress, parsed.ipv6LowAddress)) { + return containsIPv4((int) parsed.ipv6LowAddress, IPV4_BITS); + } + return false; + } + + int size() { + return ipv4Addresses.length + ipv6HighAddresses.length; + } + + private static void addParsedNetwork(IPv4Builder ipv4, IPv6Builder ipv6, ParsedNetwork parsed) { + normalizeMappedNetwork(parsed); + if (parsed.version == IPV4) { + ipv4.add(parsed.ipv4Address, parsed.prefix); + } else { + ipv6.add(parsed.ipv6HighAddress, parsed.ipv6LowAddress, parsed.prefix); + } + } + + private static void normalizeMappedNetwork(ParsedNetwork parsed) { + if (parsed.version == IPV6 + && parsed.prefix >= 96 + && isIPv4Mapped(parsed.ipv6HighAddress, parsed.ipv6LowAddress)) { + parsed.version = IPV4; + parsed.ipv4Address = (int) parsed.ipv6LowAddress; + parsed.prefix -= 96; + } + } + + private static boolean isIPv4Mapped(long high, long low) { + return high == 0 && (low >>> 32) == 0x0000ffffL; + } + + private boolean containsIPv4(int address, int prefix) { + int index = upperBoundIPv4(address, prefix) - 1; + if (index < 0) { + return false; + } + return containsIPv4( + ipv4Addresses[index], Byte.toUnsignedInt(ipv4Prefixes[index]), address, prefix); + } + + private int upperBoundIPv4(int address, int prefix) { + int left = 0; + int right = ipv4Addresses.length; + while (left < right) { + int middle = (left + right) >>> 1; + if (compareIPv4( + ipv4Addresses[middle], Byte.toUnsignedInt(ipv4Prefixes[middle]), address, prefix) + <= 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + private boolean containsIPv6(long high, long low, int prefix) { + int index = upperBoundIPv6(high, low, prefix) - 1; + if (index < 0) { + return false; + } + return containsIPv6( + ipv6HighAddresses[index], + ipv6LowAddresses[index], + Byte.toUnsignedInt(ipv6Prefixes[index]), + high, + low, + prefix); + } + + private int upperBoundIPv6(long high, long low, int prefix) { + int left = 0; + int right = ipv6HighAddresses.length; + while (left < right) { + int middle = (left + right) >>> 1; + if (compareIPv6( + ipv6HighAddresses[middle], + ipv6LowAddresses[middle], + Byte.toUnsignedInt(ipv6Prefixes[middle]), + high, + low, + prefix) + <= 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + private static int compareIPv4( + int leftAddress, int leftPrefix, int rightAddress, int rightPrefix) { + int addressComparison = Integer.compareUnsigned(leftAddress, rightAddress); + return addressComparison != 0 ? addressComparison : Integer.compare(leftPrefix, rightPrefix); + } + + private static int compareIPv6( + long leftHigh, long leftLow, int leftPrefix, long rightHigh, long rightLow, int rightPrefix) { + int highComparison = Long.compareUnsigned(leftHigh, rightHigh); + if (highComparison != 0) { + return highComparison; + } + int lowComparison = Long.compareUnsigned(leftLow, rightLow); + return lowComparison != 0 ? lowComparison : Integer.compare(leftPrefix, rightPrefix); + } + + private static boolean containsIPv4( + int networkAddress, int networkPrefix, int otherAddress, int otherPrefix) { + if (networkPrefix == 0) { + return true; + } + if (otherPrefix == 0 || networkPrefix > otherPrefix) { + return false; + } + int mask = -1 << (IPV4_BITS - networkPrefix); + return (networkAddress & mask) == (otherAddress & mask); + } + + private static boolean containsIPv6( + long networkHigh, + long networkLow, + int networkPrefix, + long otherHigh, + long otherLow, + int otherPrefix) { + if (networkPrefix == 0) { + return true; + } + if (otherPrefix == 0 || networkPrefix > otherPrefix) { + return false; + } + if (networkPrefix <= Long.SIZE) { + long mask = networkPrefix == Long.SIZE ? -1L : -1L << (Long.SIZE - networkPrefix); + return (networkHigh & mask) == (otherHigh & mask); + } + int lowPrefix = networkPrefix - Long.SIZE; + long mask = lowPrefix == Long.SIZE ? -1L : -1L << (Long.SIZE - lowPrefix); + return networkHigh == otherHigh && (networkLow & mask) == (otherLow & mask); + } + + private static int maskIPv4(int address, int prefix) { + return prefix == 0 ? 0 : address & (-1 << (IPV4_BITS - prefix)); + } + + private static long maskIPv6High(long high, int prefix) { + if (prefix == 0) { + return 0; + } + if (prefix >= Long.SIZE) { + return high; + } + return high & (-1L << (Long.SIZE - prefix)); + } + + private static long maskIPv6Low(long low, int prefix) { + if (prefix <= Long.SIZE) { + return 0; + } + int lowPrefix = prefix - Long.SIZE; + return lowPrefix == Long.SIZE ? low : low & (-1L << (Long.SIZE - lowPrefix)); + } + + private record IPv4Networks(int[] addresses, byte[] prefixes) {} + + private record IPv6Networks(long[] highAddresses, long[] lowAddresses, byte[] prefixes) {} + + private static final class IPv4Builder { + private long[] networks = new long[16]; + private int size; + + void add(int address, int prefix) { + if (size == networks.length) { + networks = Arrays.copyOf(networks, size * 2); + } + networks[size++] = encode(address, prefix); + } + + IPv4Networks build() { + if (size == 0) { + return new IPv4Networks(new int[0], new byte[0]); + } + Arrays.sort(networks, 0, size); + int summarizedSize = summarize(); + int[] addresses = new int[summarizedSize]; + byte[] prefixes = new byte[summarizedSize]; + for (int index = 0; index < summarizedSize; index++) { + addresses[index] = decodeAddress(networks[index]); + prefixes[index] = (byte) decodePrefix(networks[index]); + } + return new IPv4Networks(addresses, prefixes); + } + + private int summarize() { + int outputSize = 0; + for (int index = 0; index < size; index++) { + int address = decodeAddress(networks[index]); + int prefix = decodePrefix(networks[index]); + if (outputSize > 0) { + int previousAddress = decodeAddress(networks[outputSize - 1]); + int previousPrefix = decodePrefix(networks[outputSize - 1]); + if (containsIPv4(previousAddress, previousPrefix, address, prefix)) { + continue; + } + } + + networks[outputSize++] = networks[index]; + while (outputSize >= 2) { + int firstAddress = decodeAddress(networks[outputSize - 2]); + int firstPrefix = decodePrefix(networks[outputSize - 2]); + int secondAddress = decodeAddress(networks[outputSize - 1]); + int secondPrefix = decodePrefix(networks[outputSize - 1]); + if (firstPrefix != secondPrefix + || firstPrefix == 0 + || firstAddress != maskIPv4(firstAddress, firstPrefix - 1) + || Integer.toUnsignedLong(firstAddress) + (1L << (IPV4_BITS - firstPrefix)) + != Integer.toUnsignedLong(secondAddress)) { + break; + } + networks[outputSize - 2] = encode(firstAddress, firstPrefix - 1); + outputSize--; + } + } + return outputSize; + } + + private static long encode(int address, int prefix) { + return ((long) (address ^ Integer.MIN_VALUE) << 8) | prefix; + } + + private static int decodeAddress(long network) { + return ((int) (network >> 8)) ^ Integer.MIN_VALUE; + } + + private static int decodePrefix(long network) { + return (int) (network & 0xff); + } + } + + private static final class IPv6Builder { + private long[] highAddresses = new long[16]; + private long[] lowAddresses = new long[16]; + private byte[] prefixes = new byte[16]; + private int size; + + void add(long high, long low, int prefix) { + if (size == highAddresses.length) { + int capacity = size * 2; + highAddresses = Arrays.copyOf(highAddresses, capacity); + lowAddresses = Arrays.copyOf(lowAddresses, capacity); + prefixes = Arrays.copyOf(prefixes, capacity); + } + highAddresses[size] = high; + lowAddresses[size] = low; + prefixes[size] = (byte) prefix; + size++; + } + + IPv6Networks build() { + if (size == 0) { + return new IPv6Networks(new long[0], new long[0], new byte[0]); + } + sort(0, size - 1); + int summarizedSize = summarize(); + return new IPv6Networks( + Arrays.copyOf(highAddresses, summarizedSize), + Arrays.copyOf(lowAddresses, summarizedSize), + Arrays.copyOf(prefixes, summarizedSize)); + } + + private int summarize() { + int outputSize = 0; + for (int index = 0; index < size; index++) { + long high = highAddresses[index]; + long low = lowAddresses[index]; + int prefix = Byte.toUnsignedInt(prefixes[index]); + if (outputSize > 0 + && containsIPv6( + highAddresses[outputSize - 1], + lowAddresses[outputSize - 1], + Byte.toUnsignedInt(prefixes[outputSize - 1]), + high, + low, + prefix)) { + continue; + } + + highAddresses[outputSize] = high; + lowAddresses[outputSize] = low; + prefixes[outputSize] = (byte) prefix; + outputSize++; + while (outputSize >= 2) { + int firstIndex = outputSize - 2; + int secondIndex = outputSize - 1; + int firstPrefix = Byte.toUnsignedInt(prefixes[firstIndex]); + int secondPrefix = Byte.toUnsignedInt(prefixes[secondIndex]); + long firstHigh = highAddresses[firstIndex]; + long firstLow = lowAddresses[firstIndex]; + if (firstPrefix != secondPrefix + || firstPrefix == 0 + || firstHigh != maskIPv6High(firstHigh, firstPrefix - 1) + || firstLow != maskIPv6Low(firstLow, firstPrefix - 1) + || !isAdjacent( + firstHigh, + firstLow, + firstPrefix, + highAddresses[secondIndex], + lowAddresses[secondIndex])) { + break; + } + prefixes[firstIndex] = (byte) (firstPrefix - 1); + highAddresses[firstIndex] = maskIPv6High(firstHigh, firstPrefix - 1); + lowAddresses[firstIndex] = maskIPv6Low(firstLow, firstPrefix - 1); + outputSize--; + } + } + return outputSize; + } + + private static boolean isAdjacent( + long firstHigh, long firstLow, int prefix, long secondHigh, long secondLow) { + int bit = IPV6_BITS - prefix; + if (bit >= Long.SIZE) { + long expectedHigh = firstHigh | (1L << (bit - Long.SIZE)); + return expectedHigh == secondHigh && firstLow == secondLow; + } + long expectedLow = firstLow | (1L << bit); + return firstHigh == secondHigh && expectedLow == secondLow; + } + + private void sort(int left, int right) { + while (left < right) { + int first = left; + int last = right; + int pivotIndex = (left + right) >>> 1; + long pivotHigh = highAddresses[pivotIndex]; + long pivotLow = lowAddresses[pivotIndex]; + int pivotPrefix = Byte.toUnsignedInt(prefixes[pivotIndex]); + while (first <= last) { + while (compareAt(first, pivotHigh, pivotLow, pivotPrefix) < 0) { + first++; + } + while (compareAt(last, pivotHigh, pivotLow, pivotPrefix) > 0) { + last--; + } + if (first <= last) { + swap(first, last); + first++; + last--; + } + } + if (last - left < right - first) { + if (left < last) { + sort(left, last); + } + left = first; + } else { + if (first < right) { + sort(first, right); + } + right = last; + } + } + } + + private int compareAt(int index, long high, long low, int prefix) { + return compareIPv6( + highAddresses[index], + lowAddresses[index], + Byte.toUnsignedInt(prefixes[index]), + high, + low, + prefix); + } + + private void swap(int first, int second) { + if (first == second) { + return; + } + long high = highAddresses[first]; + highAddresses[first] = highAddresses[second]; + highAddresses[second] = high; + long low = lowAddresses[first]; + lowAddresses[first] = lowAddresses[second]; + lowAddresses[second] = low; + byte prefix = prefixes[first]; + prefixes[first] = prefixes[second]; + prefixes[second] = prefix; + } + } + + private static final class ParsedNetwork { + private int version; + private int prefix; + private int ipv4Address; + private long ipv6HighAddress; + private long ipv6LowAddress; + + private void reset() { + version = 0; + prefix = 0; + ipv4Address = 0; + ipv6HighAddress = 0; + ipv6LowAddress = 0; + } + } + + private static final class Parser { + private Parser() {} + + private static boolean parseStrictIPv4Address(String value, ParsedNetwork output) { + output.reset(); + int length = value.length(); + int position = 0; + int address = 0; + for (int part = 0; part < 4; part++) { + int start = position; + int number = 0; + while (position < length) { + char character = value.charAt(position); + if (character < '0' || character > '9') { + break; + } + number = number * 10 + character - '0'; + if (number > 255) { + return false; + } + position++; + } + if (position == start) { + return false; + } + address = (address << 8) | number; + if (part < 3) { + if (position >= length || value.charAt(position) != '.') { + return false; + } + position++; + } + } + if (position != length) { + return false; + } + output.version = IPV4; + output.prefix = IPV4_BITS; + output.ipv4Address = address; + return true; + } + + private static boolean parseBaseNetwork(String value, ParsedNetwork output) { + output.reset(); + if (value == null) { + return false; + } + int start = trimStart(value, 0, value.length()); + int end = trimEnd(value, start, value.length()); + if (start == end) { + return false; + } + + int slash = -1; + for (int index = start; index < end; index++) { + if (value.charAt(index) == '/') { + if (slash >= 0) { + return false; + } + slash = index; + } + } + + Boolean ipv4 = looksLikeIPv4(value, start, end); + if (ipv4 == null) { + return false; + } + int maxPrefix = ipv4 ? IPV4_BITS : IPV6_BITS; + int prefix = slash < 0 ? maxPrefix : parsePrefix(value, slash + 1, end, maxPrefix); + if (prefix < 0) { + return false; + } + int addressEnd = slash < 0 ? end : slash; + if (ipv4) { + long address = parseIPv4(value, start, addressEnd); + if (address < 0) { + return parseLegacyIPv4(value, start, addressEnd, prefix, output); + } + output.version = IPV4; + output.prefix = prefix; + output.ipv4Address = maskIPv4((int) address, prefix); + return true; + } + if (!parseIPv6(value, start, addressEnd, output)) { + return false; + } + output.version = IPV6; + output.prefix = prefix; + output.ipv6HighAddress = maskIPv6High(output.ipv6HighAddress, prefix); + output.ipv6LowAddress = maskIPv6Low(output.ipv6LowAddress, prefix); + return true; + } + + private static Boolean looksLikeIPv4(String value, int start, int end) { + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character == '.') { + return true; + } + if (character == ':') { + return false; + } + } + return null; + } + + private static int parsePrefix(String value, int start, int end, int maximum) { + int number = 0; + int digits = 0; + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character < '0' || character > '9') { + break; + } + number = number * 10 + character - '0'; + digits++; + if (number > maximum) { + return -1; + } + } + return digits == 0 ? -1 : number; + } + + private static long parseIPv4(String value, int start, int end) { + int position = start; + long address = 0; + for (int part = 0; part < 4; part++) { + int partEnd = end; + if (part < 3) { + partEnd = indexOf(value, '.', position, end); + if (partEnd < 0) { + return -1; + } + } else if (indexOf(value, '.', position, end) >= 0) { + return -1; + } + int number = parseDecimalPrefix(value, position, partEnd); + if (number < 0 || number > 255) { + return -1; + } + address = (address << 8) | number; + position = partEnd + 1; + } + return address; + } + + private static boolean parseLegacyIPv4( + String value, int start, int end, int requestedPrefix, ParsedNetwork output) { + int partCount = 1; + boolean hasWildcard = false; + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character == '.') { + partCount++; + } else if (character == '*') { + hasWildcard = true; + } + } + + if (hasWildcard) { + if (partCount != 4) { + return false; + } + int position = start; + int address = 0; + int wildcardPrefix = IPV4_BITS; + boolean wildcardSeen = false; + for (int part = 0; part < 4; part++) { + int partEnd = part < 3 ? indexOf(value, '.', position, end) : end; + if (partEnd < 0) { + return false; + } + if (partEnd - position == 1 && value.charAt(position) == '*') { + if (!wildcardSeen) { + wildcardPrefix = part * Byte.SIZE; + wildcardSeen = true; + } + address <<= Byte.SIZE; + } else { + if (wildcardSeen) { + return false; + } + long parsed = parseUnsignedDecimal(value, position, partEnd, 255); + if (parsed < 0) { + return false; + } + address = (address << Byte.SIZE) | (int) parsed; + } + position = partEnd + 1; + } + output.version = IPV4; + output.prefix = Math.min(requestedPrefix, wildcardPrefix); + output.ipv4Address = maskIPv4(address, output.prefix); + return true; + } + + if (partCount != 2 && partCount != 3) { + return false; + } + int firstSeparator = indexOf(value, '.', start, end); + long first = parseUnsignedDecimal(value, start, firstSeparator, 255); + if (first < 0) { + return false; + } + + long address; + if (partCount == 2) { + long second = parseUnsignedDecimal(value, firstSeparator + 1, end, 0x00ff_ffffL); + if (second < 0) { + return false; + } + address = (first << 24) | second; + } else { + int secondSeparator = indexOf(value, '.', firstSeparator + 1, end); + long second = parseUnsignedDecimal(value, firstSeparator + 1, secondSeparator, 255); + long third = parseUnsignedDecimal(value, secondSeparator + 1, end, 0xffff); + if (second < 0 || third < 0) { + return false; + } + address = (first << 24) | (second << 16) | third; + } + output.version = IPV4; + output.prefix = requestedPrefix; + output.ipv4Address = maskIPv4((int) address, requestedPrefix); + return true; + } + + private static long parseUnsignedDecimal(String value, int start, int end, long maximum) { + if (start >= end) { + return -1; + } + long number = 0; + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character < '0' || character > '9') { + return -1; + } + number = number * 10 + character - '0'; + if (number > maximum) { + return -1; + } + } + return number; + } + + private static int parseDecimalPrefix(String value, int start, int end) { + int position = trimStart(value, start, end); + boolean negative = false; + if (position < end && (value.charAt(position) == '+' || value.charAt(position) == '-')) { + negative = value.charAt(position) == '-'; + position++; + } + int digitStart = position; + int number = 0; + while (position < end) { + char character = value.charAt(position); + if (character < '0' || character > '9') { + break; + } + number = number * 10 + character - '0'; + if (number > 255) { + return -1; + } + position++; + } + if (position == digitStart || (negative && number != 0)) { + return -1; + } + return number; + } + + private static boolean parseIPv6(String value, int start, int end, ParsedNetwork output) { + if (start < end && value.charAt(start) == '[') { + int closingBracket = lastIndexOf(value, ']', start + 1, end); + if (closingBracket >= 0) { + start++; + end = closingBracket; + } + } + if (start == end) { + return false; + } + int zoneSeparator = indexOf(value, '%', start, end); + if (zoneSeparator >= 0) { + if (zoneSeparator == end - 1) { + return false; + } + end = zoneSeparator; + } + if (end - start == 2 && value.charAt(start) == ':' && value.charAt(start + 1) == ':') { + return true; + } + + int compression = indexOfDoubleColon(value, start, end); + if (compression >= 0 && indexOfDoubleColon(value, compression + 2, end) >= 0) { + return false; + } + int leftEnd = compression < 0 ? end : compression; + int leftByteIndex = parseIPv6LeftHalf(value, start, leftEnd, output); + if (leftByteIndex < 0) { + return false; + } + return compression < 0 + || parseIPv6RightHalf(value, compression + 2, end, leftByteIndex, output); + } + + private static int parseIPv6LeftHalf(String value, int start, int end, ParsedNetwork output) { + int byteIndex = 0; + if (start == end) { + return byteIndex; + } + int position = start; + while (position <= end) { + int partEnd = indexOf(value, ':', position, end); + if (partEnd < 0) { + partEnd = end; + } + if (byteIndex >= IPV6) { + return -1; + } + if (hasFourIPv4Parts(value, position, partEnd)) { + long ipv4 = parseEmbeddedIPv4(value, position, partEnd); + if (ipv4 < 0 || byteIndex + 4 > IPV6) { + return -1; + } + for (int shift = 24; shift >= 0; shift -= 8) { + setIPv6Byte(output, byteIndex++, (int) (ipv4 >>> shift) & 0xff); + } + } else { + int hextet = parseHextet(value, position, partEnd); + if (hextet < 0 || byteIndex + 2 > IPV6) { + return -1; + } + setIPv6Byte(output, byteIndex++, hextet >>> 8); + setIPv6Byte(output, byteIndex++, hextet & 0xff); + } + if (partEnd == end) { + break; + } + position = partEnd + 1; + } + return byteIndex; + } + + private static boolean parseIPv6RightHalf( + String value, int start, int end, int leftByteIndex, ParsedNetwork output) { + if (start == end) { + return true; + } + int rightByteIndex = IPV6 - 1; + int position = end; + boolean rightmost = true; + while (position >= start) { + int partStart = lastIndexOf(value, ':', start, position); + partStart = partStart < 0 ? start : partStart + 1; + if (trimStart(value, partStart, position) == trimEnd(value, partStart, position) + || leftByteIndex > rightByteIndex) { + return false; + } + if (hasFourIPv4Parts(value, partStart, position)) { + long ipv4 = parseEmbeddedIPv4(value, partStart, position); + if (ipv4 < 0 || rightByteIndex - 3 < 0) { + return false; + } + for (int shift = 0; shift <= 24; shift += 8) { + setIPv6Byte(output, rightByteIndex--, (int) (ipv4 >>> shift) & 0xff); + } + } else { + int partEnd = position; + if (rightmost) { + partEnd = removePortInfo(value, partStart, partEnd); + partStart = trimStart(value, partStart, partEnd); + } + int hextet = parseHextet(value, partStart, partEnd); + if (hextet < 0 || rightByteIndex - 1 < 0) { + return false; + } + setIPv6Byte(output, rightByteIndex--, hextet & 0xff); + setIPv6Byte(output, rightByteIndex--, hextet >>> 8); + } + rightmost = false; + if (partStart == start) { + break; + } + position = partStart - 1; + } + return true; + } + + private static long parseEmbeddedIPv4(String value, int start, int end) { + int position = start; + long address = 0; + for (int part = 0; part < 4; part++) { + int partEnd = part < 3 ? indexOf(value, '.', position, end) : end; + if (partEnd < 0 || (part == 3 && indexOf(value, '.', position, end) >= 0)) { + return -1; + } + int number = parseNumber(value, position, partEnd); + if (number < 0 || number > 255) { + return -1; + } + address = (address << 8) | number; + position = partEnd + 1; + } + return address; + } + + private static int parseNumber(String value, int start, int end) { + int position = trimStart(value, start, end); + int trimmedEnd = trimEnd(value, position, end); + if (position == trimmedEnd) { + return 0; + } + if (position + 2 < trimmedEnd && value.charAt(position) == '0') { + int radix = switch (value.charAt(position + 1)) { + case 'b', 'B' -> 2; + case 'o', 'O' -> 8; + case 'x', 'X' -> 16; + default -> 0; + }; + if (radix != 0) { + return parseRadixNumber(value, position + 2, trimmedEnd, radix); + } + } + + boolean negative = false; + if (value.charAt(position) == '+' || value.charAt(position) == '-') { + negative = value.charAt(position) == '-'; + position++; + } + double number = 0; + int digits = 0; + while (position < trimmedEnd && isDecimalDigit(value.charAt(position))) { + number = number * 10 + value.charAt(position++) - '0'; + digits++; + } + if (position < trimmedEnd && value.charAt(position) == '.') { + position++; + double decimalPlace = 0.1; + while (position < trimmedEnd && isDecimalDigit(value.charAt(position))) { + number += (value.charAt(position++) - '0') * decimalPlace; + decimalPlace *= 0.1; + digits++; + } + } + if (digits == 0) { + return -1; + } + if (position < trimmedEnd + && (value.charAt(position) == 'e' || value.charAt(position) == 'E')) { + position++; + boolean negativeExponent = false; + if (position < trimmedEnd + && (value.charAt(position) == '+' || value.charAt(position) == '-')) { + negativeExponent = value.charAt(position) == '-'; + position++; + } + int exponent = 0; + int exponentDigits = 0; + while (position < trimmedEnd && isDecimalDigit(value.charAt(position))) { + exponent = Math.min(1000, exponent * 10 + value.charAt(position++) - '0'); + exponentDigits++; + } + if (exponentDigits == 0) { + return -1; + } + number *= Math.pow(10, negativeExponent ? -exponent : exponent); + } + if (position != trimmedEnd) { + return -1; + } + if (negative) { + number = -number; + } + return number >= 0 && number <= 255 && number == Math.rint(number) ? (int) number : -1; + } + + private static int parseRadixNumber(String value, int start, int end, int radix) { + int number = 0; + for (int position = start; position < end; position++) { + int digit = Character.digit(value.charAt(position), radix); + if (digit < 0) { + return -1; + } + number = number * radix + digit; + if (number > 255) { + return -1; + } + } + return number; + } + + private static boolean isDecimalDigit(char character) { + return character >= '0' && character <= '9'; + } + + private static boolean hasFourIPv4Parts(String value, int start, int end) { + int dots = 0; + for (int index = start; index < end; index++) { + if (value.charAt(index) == '.') { + dots++; + } + } + return dots == 3; + } + + private static int parseHextet(String value, int start, int end) { + int trimmedStart = trimStart(value, start, end); + int trimmedEnd = trimEnd(value, trimmedStart, end); + int trimmedLength = trimmedEnd - trimmedStart; + if (trimmedLength < 1 || trimmedLength > 4) { + return -1; + } + int parsed = 0; + for (int index = start; index < end; index++) { + char character = value.charAt(index); + int digit; + if (character >= '0' && character <= '9') { + digit = character - '0'; + } else if (character >= 'a' && character <= 'f') { + digit = character - 'a' + 10; + } else if (character >= 'A' && character <= 'F') { + digit = character - 'A' + 10; + } else { + return -1; + } + parsed = (parsed << 4) | digit; + } + return parsed; + } + + private static int removePortInfo(String value, int start, int end) { + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character == '#' || character == 'p' || character == '.') { + return trimEnd(value, start, index); + } + } + return end; + } + + private static void setIPv6Byte(ParsedNetwork output, int index, int value) { + if (index < Long.BYTES) { + output.ipv6HighAddress |= (long) value << ((Long.BYTES - 1 - index) * Byte.SIZE); + } else { + output.ipv6LowAddress |= (long) value << ((IPV6 - 1 - index) * Byte.SIZE); + } + } + + private static int indexOf(String value, char target, int start, int end) { + for (int index = start; index < end; index++) { + if (value.charAt(index) == target) { + return index; + } + } + return -1; + } + + private static int lastIndexOf(String value, char target, int start, int end) { + for (int index = end - 1; index >= start; index--) { + if (value.charAt(index) == target) { + return index; + } + } + return -1; + } + + private static int indexOfDoubleColon(String value, int start, int end) { + for (int index = start; index + 1 < end; index++) { + if (value.charAt(index) == ':' && value.charAt(index + 1) == ':') { + return index; + } + } + return -1; + } + + private static int trimStart(String value, int start, int end) { + while (start < end && isWhitespace(value.charAt(start))) { + start++; + } + return start; + } + + private static int trimEnd(String value, int start, int end) { + while (end > start && isWhitespace(value.charAt(end - 1))) { + end--; + } + return end; + } + + private static boolean isWhitespace(char character) { + return Character.isWhitespace(character) || Character.isSpaceChar(character); + } + } +} diff --git a/agent_api/src/test/java/helpers/IPAccessControllerTest.java b/agent_api/src/test/java/helpers/IPAccessControllerTest.java index f7c030339..1ec7fa563 100644 --- a/agent_api/src/test/java/helpers/IPAccessControllerTest.java +++ b/agent_api/src/test/java/helpers/IPAccessControllerTest.java @@ -1,8 +1,10 @@ package helpers; +import com.google.gson.Gson; import dev.aikido.agent_api.background.Endpoint; import dev.aikido.agent_api.helpers.IPAccessController; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -74,6 +76,26 @@ public void testBlocksRequestIfNotAllowedIpAddress() { assertFalse(IPAccessController.ipAllowedToAccessRoute("3.4.5.6", endpoints)); } + @Test + public void testUsesCachedAllowedIpMatcher() { + List allowedIPAddresses = new ArrayList<>(List.of("1.2.3.4")); + Endpoint endpoint = genEndpoint(allowedIPAddresses); + allowedIPAddresses.clear(); + + assertTrue(endpoint.isIpAllowed("1.2.3.4")); + } + + @Test + public void testDeserializedEndpointBuildsAllowedIpMatcher() { + Endpoint endpoint = new Gson().fromJson( + "{\"method\":\"GET\",\"route\":\"/\",\"allowedIPAddresses\":[\"1.2.3.4\"]}", + Endpoint.class + ); + + assertTrue(endpoint.isIpAllowed("1.2.3.4")); + assertFalse(endpoint.isIpAllowed("3.4.5.6")); + } + @Test public void testChecksEveryMatchingEndpoint() { List endpoints = List.of( diff --git a/agent_api/src/test/java/helpers/net/IPMatcherCompatibilityTest.java b/agent_api/src/test/java/helpers/net/IPMatcherCompatibilityTest.java new file mode 100644 index 000000000..472fa2e4e --- /dev/null +++ b/agent_api/src/test/java/helpers/net/IPMatcherCompatibilityTest.java @@ -0,0 +1,324 @@ +package helpers.net; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.aikido.agent_api.helpers.net.IPList; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class IPMatcherCompatibilityTest { + @Test + void matchesSingleIPv4Addresses() { + IPList matcher = new IPList(List.of( + "192.168.0.0/32", + "192.168.0.3/32", + "192.168.0.24/32", + "192.168.0.52/32", + "192.168.0.123/32", + "192.168.0.124/32", + "192.168.0.125/32", + "192.168.0.170/32", + "192.168.0.171/32", + "192.168.0.222/32", + "192.168.0.234/32", + "192.168.0.255/32")); + + assertFalse(matcher.matches("192.168.0.254")); + assertFalse(matcher.matches("192.168.0.1")); + assertTrue(matcher.matches("192.168.0.255")); + assertTrue(matcher.matches("192.168.0.24")); + } + + @Test + void summarizesRangesAndContainedAddresses() { + IPList matcher = new IPList(List.of( + "192.168.0.0/24", + "192.168.0.3/32", + "192.168.0.24/32", + "192.168.0.52/32", + "192.168.0.123/32", + "192.168.0.124/32", + "192.168.0.125/32", + "192.168.0.170/32", + "192.168.0.171/32", + "192.168.0.222/32", + "192.168.0.234/32", + "192.168.0.255/32")); + + assertEquals(1, matcher.length()); + assertTrue(matcher.matches("192.168.0.254")); + assertTrue(matcher.matches("192.168.0.234")); + assertFalse(matcher.matches("10.0.0.1")); + } + + @Test + void ignoresInvalidRanges() { + IPList matcher = new IPList(List.of( + "192.168.0.0/24", + "foobar", + "0.a.0.0/32", + "123.123.123.123/1999", + "", + ",,,", + "192.168.0.255")); + + assertTrue(matcher.matches("192.168.0.254")); + assertTrue(matcher.matches("192.168.0.255")); + assertTrue(matcher.matches("192.168.0.1/32")); + assertFalse(matcher.matches("foobar")); + assertFalse(matcher.matches("")); + assertFalse(matcher.matches("1")); + assertFalse(matcher.matches(null)); + assertFalse(matcher.matches("10.0.0.1")); + } + + @Test + void emptyMatcherNeverMatches() { + IPList matcher = new IPList(List.of()); + + assertFalse(matcher.matches("192.168.2.1")); + assertFalse(matcher.matches("foobar")); + } + + @Test + void matchesIPv6RangesAndBracketedAddresses() { + IPList matcher = new IPList(List.of( + "2002:db8::/32", + "2001:db8::1/128", + "2001:db8::2/128", + "2001:db8::3/128", + "2001:db8::4/128", + "2001:db8::5/128", + "2001:db8::6/128", + "2001:db8::7/128", + "2001:db8::8/128", + "2001:db8::9/128", + "2001:db8::a/128", + "2001:db8::b/128", + "2001:db8::c/128", + "2001:db8::d/128", + "2001:db8::e/128", + "[2001:db8::f]", + "2001:db9::abc")); + + assertTrue(matcher.matches("2001:db8::1")); + assertFalse(matcher.matches("2001:db8::0")); + assertTrue(matcher.matches("2001:db8::f")); + assertTrue(matcher.matches("[2001:db8::f]")); + assertFalse(matcher.matches("2001:db8::10")); + assertTrue(matcher.matches("2002:db8::1")); + assertTrue(matcher.matches("2002:db8::2f:2")); + assertTrue(matcher.matches("2001:db9::abc")); + } + + @Test + void keepsIPv4AndIPv6AddressSpacesSeparate() { + IPList matcher = new IPList(List.of("2002:db8::/32", "10.0.0.0/8")); + + assertFalse(matcher.matches("2001:db8::1")); + assertTrue(matcher.matches("2002:db8::1")); + assertTrue(matcher.matches("10.0.0.1")); + assertTrue(matcher.matches("10.0.0.255")); + assertFalse(matcher.matches("192.168.1.1")); + } + + @Test + void supportsIncrementalAddsWithoutMutatingPublishedMatcherState() { + IPList matcher = new IPList(); + + assertFalse(matcher.matches("2002:db8::1")); + matcher.add("2002:db8::/32"); + matcher.add("10.0.0.0/8"); + + assertFalse(matcher.matches("2001:db8::1")); + assertTrue(matcher.matches("2002:db8::1")); + assertTrue(matcher.matches("10.0.0.1")); + assertFalse(matcher.matches("192.168.1.1")); + } + + @Test + void preservesEmbeddedIPv4AddressForms() { + IPList matcher = new IPList(List.of( + "64:ff9b::192.0.2.1", + "::ffff:192.0.2.1", + "::ffff:127.0.0.1", + "::ffff:0.0.0.0", + "::ffff:0:0:0:0", + "192.0.2.55")); + + assertTrue(matcher.matches("64:ff9b::c000:201")); + assertFalse(matcher.matches("::ffff:192.0.2.1garbage")); + assertFalse(matcher.matches("::ffff:192.0.2.1abc")); + assertTrue(matcher.matches("::ffff:192.0.2.0x1")); + assertTrue(matcher.matches("[::ffff:127.0.0.1]")); + assertTrue(matcher.matches("::ffff:7f00:1")); + assertTrue(matcher.matches("::ffff:0.0.0.0")); + assertTrue(matcher.matches("::ffff:0:0:0:0")); + assertTrue(matcher.matches("127.0.0.1")); + assertTrue(matcher.matches("::ffff:192.0.2.55")); + assertFalse(matcher.matches("::ffff:123")); + } + + @ParameterizedTest + @MethodSource("cidrCases") + void appliesEveryIPv4CidrBoundary(String network, String address, boolean expected) { + assertEquals(expected, new IPList(List.of(network)).matches(address)); + } + + static Stream cidrCases() { + return Stream.of( + Arguments.of("123.2.0.2/0", "1.1.1.1", true), + Arguments.of("123.2.0.2/1", "1.1.1.1", true), + Arguments.of("123.2.0.2/2", "1.1.1.1", false), + Arguments.of("123.2.0.2/3", "123.3.0.1", true), + Arguments.of("123.2.0.2/4", "123.3.0.1", true), + Arguments.of("123.2.0.2/5", "123.3.0.1", true), + Arguments.of("123.2.0.2/6", "123.3.0.1", true), + Arguments.of("123.2.0.2/7", "123.3.0.1", true), + Arguments.of("123.2.0.2/8", "123.3.0.1", true), + Arguments.of("123.2.0.2/9", "123.3.0.1", true), + Arguments.of("123.2.0.2/10", "123.3.0.1", true), + Arguments.of("123.2.0.2/11", "123.3.0.1", true), + Arguments.of("123.2.0.2/12", "123.3.0.1", true), + Arguments.of("123.2.0.2/13", "123.3.0.1", true), + Arguments.of("123.2.0.2/14", "123.3.0.1", true), + Arguments.of("123.2.0.2/15", "123.3.0.1", true), + Arguments.of("123.2.0.2/16", "123.3.0.1", false), + Arguments.of("123.2.0.2/17", "123.2.0.1", true), + Arguments.of("123.2.0.2/18", "123.2.0.1", true), + Arguments.of("123.2.0.2/19", "123.2.0.1", true), + Arguments.of("123.2.0.2/20", "123.2.0.1", true), + Arguments.of("123.2.0.2/21", "123.2.0.1", true), + Arguments.of("123.2.0.2/22", "123.2.0.1", true), + Arguments.of("123.2.0.2/23", "123.2.0.1", true), + Arguments.of("123.2.0.2/24", "123.2.0.1", true), + Arguments.of("123.2.0.2/25", "123.2.0.1", true), + Arguments.of("123.2.0.2/26", "123.2.0.1", true), + Arguments.of("123.2.0.2/27", "123.2.0.1", true), + Arguments.of("123.2.0.2/29", "123.2.0.1", true), + Arguments.of("123.2.0.2/30", "123.2.0.1", true), + Arguments.of("123.2.0.2/31", "123.2.0.1", false), + Arguments.of("123.2.0.2/32", "123.2.0.2", true)); + } + + @Test + void matchesNetworksOnlyWhenTheStoredNetworkContainsThem() { + IPList matcher = new IPList(List.of("192.168.0.123/24", "2001:db8:1::beef/48")); + + assertTrue(matcher.matches("192.168.0.128/25")); + assertFalse(matcher.matches("192.168.0.0/16")); + assertTrue(matcher.matches("2001:db8:1:ffff::1")); + assertFalse(matcher.matches("2001:db8:2::1")); + } + + @Test + void allowsBothAddressFamiliesWithZeroLengthPrefixes() { + IPList matcher = new IPList(List.of("0.0.0.0/0", "::/0")); + + assertTrue(matcher.matches("1.2.3.4")); + assertTrue(matcher.matches("::1")); + assertTrue(matcher.matches("::ffff:1234")); + assertTrue(matcher.matches("2002:db8::1")); + assertTrue(matcher.matches("255.255.255.255")); + } + + @Test + void sortsAndSummarizesUnorderedNetworks() { + IPList matcher = new IPList(List.of( + "2001:db8:0:3::/64", + "10.0.3.0/24", + "2001:db8:0:1::/64", + "10.0.1.0/24", + "2001:db8:0:2::/64", + "10.0.0.0/24", + "2001:db8:0:0::/64", + "10.0.2.0/24")); + + assertEquals(2, matcher.length()); + assertTrue(matcher.matches("10.0.3.255")); + assertTrue(matcher.matches("2001:db8:0:3:ffff:ffff:ffff:ffff")); + assertFalse(matcher.matches("10.0.4.0")); + assertFalse(matcher.matches("2001:db8:0:4::")); + } + + @Test + void mergesAdjacentRangesAtBothAddressSpaceBoundaries() { + IPList matcher = new IPList(List.of("224.0.0.0/4", "240.0.0.0/4", "e000::/4", "f000::/4")); + + assertEquals(2, matcher.length()); + assertTrue(matcher.matches("224.0.0.1")); + assertTrue(matcher.matches("255.255.255.255")); + assertFalse(matcher.matches("223.255.255.255")); + assertTrue(matcher.matches("e000::1")); + assertTrue(matcher.matches("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + assertFalse(matcher.matches("dfff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + } + + @Test + void preservesNodeParserPortAndBracketForms() { + IPList ipv4 = new IPList(List.of("192.0.2.1")); + assertTrue(ipv4.matches("192.0.2.1:80")); + + IPList ipv6 = new IPList(List.of("2001:db8::1")); + assertTrue(ipv6.matches("[2001:db8::1]:443")); + assertTrue(ipv6.matches("2001:db8::1p443")); + assertTrue(ipv6.matches("2001:db8::1#443")); + assertTrue(ipv6.matches("2001:db8::1.443")); + assertFalse(ipv6.matches("2001:db8::2p443")); + } + + @Test + void preservesNodeParserCidrAndShorthandBehavior() { + IPList matcher = new IPList(List.of(" 192.168.2.1/24suffix ", "2001:db8")); + + assertTrue(matcher.matches("192.168.2.200")); + assertTrue(matcher.matches("2001:db8::")); + assertFalse(new IPList(List.of("192.168.2.1/abcde")).matches("192.168.2.1")); + assertFalse(new IPList(List.of("192.168.2.1/24/test")).matches("192.168.2.1")); + } + + @Test + void ignoresIPv6ZoneIdentifiersDuringMatching() { + IPList matcher = new IPList(List.of("::1", "fe80::/10", "2001:db8::/32")); + + assertTrue(matcher.matches("fe80::1%eth0")); + assertTrue(matcher.matches("fe80::1%3")); + assertTrue(matcher.matches("[fe80::1%eth0]")); + assertTrue(matcher.matches("::1%lo")); + assertTrue(matcher.matches("2001:db8::1%eth0")); + assertFalse(matcher.matches("2001:db9::1%eth0")); + assertFalse(matcher.matches("fe80::1%")); + } + + @Test + void mapsIPv4MappedIPv6RequestsToIPv4Networks() { + IPList addressMatcher = new IPList(List.of("192.0.2.1")); + assertTrue(addressMatcher.matches("192.0.2.1")); + assertTrue(addressMatcher.matches("::ffff:192.0.2.1")); + assertTrue(addressMatcher.matches("::ffff:c000:201")); + assertFalse(addressMatcher.matches("::ffff:192.0.2.2")); + + IPList rangeMatcher = new IPList(List.of("192.0.2.0/24")); + assertTrue(rangeMatcher.matches("::ffff:192.0.2.1")); + assertTrue(rangeMatcher.matches("::ffff:192.0.2.255")); + assertFalse(rangeMatcher.matches("::ffff:192.0.3.1")); + } + + @Test + void retainsJavaCompatibilityForStoredMappedNetworks() { + IPList addressMatcher = new IPList(List.of("::ffff:192.0.2.1")); + assertTrue(addressMatcher.matches("192.0.2.1")); + assertTrue(addressMatcher.matches("::ffff:192.0.2.1")); + + IPList rangeMatcher = new IPList(List.of("::ffff:10.0.0.0/104")); + assertTrue(rangeMatcher.matches("10.1.2.3")); + assertTrue(rangeMatcher.matches("::ffff:10.1.2.3")); + assertFalse(rangeMatcher.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 7eefb38c8..1ef94ccb3 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/IsPrivateIPTest.java @@ -29,6 +29,7 @@ void testPrivateIPv4Addresses() { assertTrue(isPrivateIp("10.255.255.255")); assertTrue(isPrivateIp("100.64.0.0")); assertTrue(isPrivateIp("100.64.0.1")); + assertTrue(isPrivateIp("100.100.100.200")); assertTrue(isPrivateIp("100.127.255.254")); assertTrue(isPrivateIp("100.127.255.255")); assertTrue(isPrivateIp("127.0.0.0")); @@ -41,6 +42,7 @@ void testPrivateIPv4Addresses() { assertTrue(isPrivateIp("127.255.255.255")); assertTrue(isPrivateIp("169.254.0.0")); assertTrue(isPrivateIp("169.254.0.1")); + assertTrue(isPrivateIp("169.254.169.254")); assertTrue(isPrivateIp("169.254.255.254")); assertTrue(isPrivateIp("169.254.255.255")); assertTrue(isPrivateIp("172.16.0.0")); @@ -138,6 +140,10 @@ void testPrivateIPv6Addresses() { assertTrue(isPrivateIp("fe80::")); assertTrue(isPrivateIp("fe80::1")); assertTrue(isPrivateIp("fe80::abc:1")); + assertTrue(isPrivateIp("fe80::1%eth0")); + assertTrue(isPrivateIp("fe80::1%dummy0")); + assertTrue(isPrivateIp("fe80:0:0:0:0:0:0:1%3")); + assertTrue(isPrivateIp("::1%lo")); assertTrue(isPrivateIp("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); assertTrue(isPrivateIp("fc00::")); assertTrue(isPrivateIp("fc00::1")); @@ -180,6 +186,9 @@ void testInvalidAddresses() { assertFalse(isPrivateIp("invalid-ip")); assertFalse(isPrivateIp("256.256.256.256")); // Invalid IPv4 assertFalse(isPrivateIp("::g")); // Invalid IPv6 + assertFalse(isPrivateIp("100::ffff::")); + assertFalse(isPrivateIp("::ffff:0.0.255.255.255")); + assertFalse(isPrivateIp("::ffff:0.255.255.255.255")); } }