Repository navigation
Back IPList with a Java port of zen-internals' Rust IP matcher - #389
Conversation
| IPAddress block = address.isMultiple() ? address.assignPrefixForSingleBlock() : address; | ||
| if (block != null) { | ||
| addMatcherNetwork(networks, block.toNormalizedString()); |
There was a problem hiding this comment.
🟡 Medium - Non-prefix IP ranges are silently dropped from firewall lists
A firewall or allowlist response can contain an IPAddressString range such as 1-200.3-200.4-200, which the previous IPList stored as a range. The new fallback converts multi-address values only with assignPrefixForSingleBlock() and discards ranges that cannot be represented by one prefix, so configured blocked addresses are not blocked and configured allowed addresses can be denied.
Show fix
Preserve the full inet.ipaddr range when a list entry is not a single CIDR block, or reject and surface unsupported range syntax before installing the configuration rather than silently omitting it. The matcher needs a representation for arbitrary ranges (or the existing library-backed range structure must remain in the matching path).
More info - Reply on this comment to give feedback or ignore the issue.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
ec9097b to
abc0429
Compare
abc0429 to
c432b5f
Compare
| private static int parseIntRange(String value, int start, int end, int max) { | ||
| int number = 0; | ||
| int digits = 0; | ||
| for (int index = start; index < end && isAsciiDigit(value.charAt(index)); index++) { | ||
| number = number * 10 + (value.charAt(index) - '0'); | ||
| digits++; | ||
| if (number > max) { | ||
| return -1; | ||
| } | ||
| } | ||
| return digits > 0 && number <= max ? number : -1; | ||
| } |
There was a problem hiding this comment.
🟡 Medium - Malformed CIDRs can widen endpoint allowlists to all IPs
A consumer supplies an endpoint allowlist containing a malformed CIDR such as 192.168.1.1/0garbage or an overflowing prefix such as /4294967296 (Trigger). parseIntRange stops at the first non-digit and performs the accumulation in a 32-bit int, so these values are accepted as prefix lengths 0 and 0 respectively, and the address is then masked to 0.0.0.0/0 (Mechanism). Endpoint.isIpAllowed therefore accepts every remote address despite the configured allowlist naming a specific or invalid network (Consequence).
Show fix
| private static int parseIntRange(String value, int start, int end, int max) { | |
| int number = 0; | |
| int digits = 0; | |
| for (int index = start; index < end && isAsciiDigit(value.charAt(index)); index++) { | |
| number = number * 10 + (value.charAt(index) - '0'); | |
| digits++; | |
| if (number > max) { | |
| return -1; | |
| } | |
| } | |
| return digits > 0 && number <= max ? number : -1; | |
| } | |
| private static int parseIntRange(String value, int start, int end, int max) { | |
| int number = 0; | |
| if (start >= end) { | |
| return -1; | |
| } | |
| for (int index = start; index < end; index++) { | |
| char character = value.charAt(index); | |
| if (!isAsciiDigit(character)) { | |
| return -1; | |
| } | |
| int digit = character - '0'; | |
| if (number > (max - digit) / 10) { | |
| return -1; | |
| } | |
| number = number * 10 + digit; | |
| } | |
| return number; | |
| } |
More info - Reply on this comment to give feedback or ignore the issue.
c432b5f to
c6fa647
Compare
| if (leftByteIndex == NO_BYTE_INDEX) { | ||
| return false; | ||
| } | ||
| return splitIndex < 0 || parseIpv6RightHalf(network, value, splitIndex + 2, end, leftByteIndex); |
There was a problem hiding this comment.
🟡 Medium - Incomplete IPv6 allowlist entries are accepted as different addresses
A cloud-supplied IP list contains an uncompressed IPv6 value with fewer than eight hextets, such as 2001:db8:1 (Trigger). parseIpv6 returns success whenever the left parser returns, without requiring that the uncompressed address filled all 16 bytes, so the missing suffix is silently treated as zero (Mechanism). The resulting /128 can match a different canonical client address and satisfy a bypass or endpoint allowlist, disabling protection or avoiding the expected 403 (Consequence).
| return splitIndex < 0 || parseIpv6RightHalf(network, value, splitIndex + 2, end, leftByteIndex); | |
| return splitIndex < 0 | |
| ? leftByteIndex == IPV6_BYTES | |
| : parseIpv6RightHalf(network, value, splitIndex + 2, end, leftByteIndex); |
More info - Reply on this comment to give feedback or ignore the issue.
| for (int index = start; index < end; index++) { | ||
| char character = value.charAt(index); | ||
| if (character == '#' || character == 'p' || character == '.') { | ||
| return trimAsciiWhitespace(value, start, index); |
There was a problem hiding this comment.
🟡 Medium - Malformed IPv6 suffixes are truncated into firewall addresses
A control-plane IP list contains a malformed IPv6 value such as ::1.2.3 or [::1]junk (Trigger). The matcher strips the suffix at the first dot or ignores text after the closing bracket before parsing, so each value is treated as the valid address ::1 (Mechanism). Endpoint/global allowlists and bypass or block lists then make an authorization decision for ::1 even though the configured entry is not that address (Consequence).
Show fix
Only remove a bracket pair when the entire input is a valid bracketed address with an explicitly supported port form, and do not truncate IPv6 components at arbitrary ., p, or # characters. Reject any remaining suffix or malformed dotted IPv6 component instead of installing or matching the parsed prefix.
More info - Reply on this comment to give feedback or ignore the issue.
043e03f to
5bc0811
Compare
5bc0811 to
80b3ff8
Compare
|
|
||
| for (int offset = 0; offset < 4; offset++) { | ||
| int index = reverse ? 3 - offset : offset; | ||
| int partStart = index == 0 ? start : index == 1 ? firstDot + 1 : index == 2 ? secondDot + 1 : thirdDot + 1; |
There was a problem hiding this comment.
The chained ternary selecting partStart makes the index mapping difficult to scan.
Show fix
| int partStart = index == 0 ? start : index == 1 ? firstDot + 1 : index == 2 ? secondDot + 1 : thirdDot + 1; | |
| int partStart; | |
| if (index == 0) { | |
| partStart = start; | |
| } else if (index == 1) { | |
| partStart = firstDot + 1; | |
| } else if (index == 2) { | |
| partStart = secondDot + 1; | |
| } else { | |
| partStart = thirdDot + 1; | |
| } |
Details
✨ AI Reasoning
The expression selects partStart through a chain of inline conditions. Each branch requires tracking another condition, making the index-to-start mapping harder to scan.
Reply on this comment to give feedback or ignore the issue.
More info
|
|
||
| public IPList() { | ||
| this.ipAddresses = new DualIPv4v6Tries(); | ||
| this(List.of()); | ||
| } | ||
|
|
||
| public IPList(Collection<String> ipsOrCIDRs) { | ||
| this.matcher = new IPMatcher(normalizeNetworks(ipsOrCIDRs)); | ||
| } | ||
|
|
||
| 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.withNetworks(List.of(ipv4IfMapped(ipOrCIDR))); |
There was a problem hiding this comment.
Each add rebuilds, sorts, and summarizes all existing networks, making repeated additions increasingly expensive and potentially quadratic overall.
Show fix
| public IPList() { | |
| this.ipAddresses = new DualIPv4v6Tries(); | |
| this(List.of()); | |
| } | |
| public IPList(Collection<String> ipsOrCIDRs) { | |
| this.matcher = new IPMatcher(normalizeNetworks(ipsOrCIDRs)); | |
| } | |
| 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.withNetworks(List.of(ipv4IfMapped(ipOrCIDR))); | |
| private final List<String> networks; | |
| public IPList() { | |
| this(List.of()); | |
| } | |
| public IPList(Collection<String> ipsOrCIDRs) { | |
| this.networks = new java.util.concurrent.CopyOnWriteArrayList<>(normalizeNetworks(ipsOrCIDRs)); | |
| this.matcher = new IPMatcher(this.networks); | |
| } | |
| public void add(String ipOrCIDR) { | |
| if (ipOrCIDR == null) { | |
| return; // Don't add if IP is null | |
| } | |
| networks.add(ipv4IfMapped(ipOrCIDR)); | |
| matcher = new IPMatcher(networks); |
Details
✨ AI Reasoning
The method is intended to add one network. The replacement constructs a new matcher from the existing matcher and that network; the constructor copies the base entries and sorts and summarizes them. Repeated additions therefore repeat work proportional to the growing collection, which can become significant for large lists.
Reply on this comment to give feedback or ignore the issue.
More info
| for (int index = start; index < end && isAsciiDigit(value.charAt(index)); index++) { | ||
| number = number * 10 + (value.charAt(index) - '0'); | ||
| digits++; | ||
| if (number > max) { | ||
| return -1; | ||
| } | ||
| } | ||
| return digits > 0 && number <= max ? number : -1; |
There was a problem hiding this comment.
parseIntRange accepts a numeric prefix and ignores trailing CIDR characters, so malformed inputs such as 10.0.0.1/32junk can match. Validate that the entire CIDR suffix was consumed.
Show fix
| for (int index = start; index < end && isAsciiDigit(value.charAt(index)); index++) { | |
| number = number * 10 + (value.charAt(index) - '0'); | |
| digits++; | |
| if (number > max) { | |
| return -1; | |
| } | |
| } | |
| return digits > 0 && number <= max ? number : -1; | |
| int index = start; | |
| for (; index < end && isAsciiDigit(value.charAt(index)); index++) { | |
| number = number * 10 + (value.charAt(index) - '0'); | |
| digits++; | |
| if (number > max) { | |
| return -1; | |
| } | |
| } | |
| return digits > 0 && index == end && number <= max ? number : -1; |
Details
✨ AI Reasoning
The parser reads digits until it encounters a non-digit and returns the parsed value without checking that it reached the end of the CIDR text. As a result, malformed inputs such as 10.0.0.1/32junk can be accepted and match, whereas the prior address-library validation rejected malformed inputs. This weakens input validation in the matcher.
Reply on this comment to give feedback or ignore the issue.
More info
| start = sliceStart(withoutBrackets); | ||
| end = sliceEnd(withoutBrackets); | ||
| if (start == end) { | ||
| return false; | ||
| } | ||
| int zoneIndex = indexOf(value, '%', start, end); | ||
| if (zoneIndex >= 0) { | ||
| if (zoneIndex == end - 1) { | ||
| return false; | ||
| } | ||
| end = zoneIndex; | ||
| } | ||
| if (end - start == 2 && value.charAt(start) == ':' && value.charAt(start + 1) == ':') { | ||
| return true; | ||
| } | ||
|
|
||
| int splitIndex = value.indexOf("::", start); | ||
| if (splitIndex + 2 > end) { | ||
| splitIndex = -1; | ||
| } | ||
| if (splitIndex >= 0) { | ||
| int nextSplit = value.indexOf("::", splitIndex + 2); | ||
| if (nextSplit >= 0 && nextSplit + 2 <= end) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| int leftLength = splitIndex >= 0 ? splitIndex : end; | ||
| int leftByteIndex = parseIpv6LeftHalf(network, value, start, leftLength); | ||
| if (leftByteIndex == NO_BYTE_INDEX) { | ||
| return false; | ||
| } | ||
| return splitIndex < 0 || parseIpv6RightHalf(network, value, splitIndex + 2, end, leftByteIndex); |
There was a problem hiding this comment.
parseIpv6 reassigns its start and end parameters while deriving the address bounds, obscuring the original input bounds. Use local variables for the adjusted bounds.
Show fix
| start = sliceStart(withoutBrackets); | |
| end = sliceEnd(withoutBrackets); | |
| if (start == end) { | |
| return false; | |
| } | |
| int zoneIndex = indexOf(value, '%', start, end); | |
| if (zoneIndex >= 0) { | |
| if (zoneIndex == end - 1) { | |
| return false; | |
| } | |
| end = zoneIndex; | |
| } | |
| if (end - start == 2 && value.charAt(start) == ':' && value.charAt(start + 1) == ':') { | |
| return true; | |
| } | |
| int splitIndex = value.indexOf("::", start); | |
| if (splitIndex + 2 > end) { | |
| splitIndex = -1; | |
| } | |
| if (splitIndex >= 0) { | |
| int nextSplit = value.indexOf("::", splitIndex + 2); | |
| if (nextSplit >= 0 && nextSplit + 2 <= end) { | |
| return false; | |
| } | |
| } | |
| int leftLength = splitIndex >= 0 ? splitIndex : end; | |
| int leftByteIndex = parseIpv6LeftHalf(network, value, start, leftLength); | |
| if (leftByteIndex == NO_BYTE_INDEX) { | |
| return false; | |
| } | |
| return splitIndex < 0 || parseIpv6RightHalf(network, value, splitIndex + 2, end, leftByteIndex); | |
| int addressStart = sliceStart(withoutBrackets); | |
| int addressEnd = sliceEnd(withoutBrackets); | |
| if (addressStart == addressEnd) { | |
| return false; | |
| } | |
| int zoneIndex = indexOf(value, '%', addressStart, addressEnd); | |
| if (zoneIndex >= 0) { | |
| if (zoneIndex == addressEnd - 1) { | |
| return false; | |
| } | |
| addressEnd = zoneIndex; | |
| } | |
| if (addressEnd - addressStart == 2 && value.charAt(addressStart) == ':' && value.charAt(addressStart + 1) == ':') { | |
| return true; | |
| } | |
| int splitIndex = value.indexOf("::", addressStart); | |
| if (splitIndex + 2 > addressEnd) { | |
| splitIndex = -1; | |
| } | |
| if (splitIndex >= 0) { | |
| int nextSplit = value.indexOf("::", splitIndex + 2); | |
| if (nextSplit >= 0 && nextSplit + 2 <= addressEnd) { | |
| return false; | |
| } | |
| } | |
| int leftLength = splitIndex >= 0 ? splitIndex : addressEnd; | |
| int leftByteIndex = parseIpv6LeftHalf(network, value, addressStart, leftLength); | |
| if (leftByteIndex == NO_BYTE_INDEX) { | |
| return false; | |
| } | |
| return splitIndex < 0 || parseIpv6RightHalf(network, value, splitIndex + 2, addressEnd, leftByteIndex); |
Details
✨ AI Reasoning
The method first replaces the input bounds after removing brackets, then may replace the end bound again when handling a zone identifier. Reusing the parameter names obscures the original bounds passed by the caller.
Reply on this comment to give feedback or ignore the issue.
More info
No description provided.