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
4 changes: 0 additions & 4 deletions agent_api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import java.util.List;

import static dev.aikido.agent_api.helpers.IPListBuilder.createIPList;
import static dev.aikido.agent_api.helpers.IPListBuilder.createIPListWithMappedAddresses;

public class Endpoint {
public record RateLimitingConfig(long maxRequests, long windowSizeInMS, boolean enabled) {}
private final String method;
private final String route;
private final RateLimitingConfig rateLimiting;
private final List<String> allowedIPAddresses;
private transient volatile IPList allowedIPMatcher;
private final boolean graphql;
private final boolean forceProtectionOff;
public Endpoint(
Expand All @@ -21,6 +22,8 @@ public Endpoint(
this.method = method;
this.route = route;
this.allowedIPAddresses = allowedIPAddresses;
// Small list, frequently accessed: add IPv4-mapped versions at creation time for fast lookups
this.allowedIPMatcher = createIPListWithMappedAddresses(allowedIPAddresses);
this.rateLimiting = new RateLimitingConfig(maxRequests, windowSizeMS, rateLimitingEnabled);
this.graphql = graphql;
this.forceProtectionOff = forceProtectionOff;
Expand All @@ -47,7 +50,15 @@ public boolean allowedIpAddressesEmpty() {
return allowedIPAddresses == null || allowedIPAddresses.size() == 0;
}

// Gson can skip the constructor, so create the matcher on first use.
private IPList getOrCreateAllowedIPMatcher() {
if (allowedIPMatcher == null) {
allowedIPMatcher = createIPListWithMappedAddresses(allowedIPAddresses);
}
return allowedIPMatcher;
}

public boolean isIpAllowed(String ip) {
return createIPList(allowedIPAddresses).matches(ip);
return getOrCreateAllowedIPMatcher().matches(ip);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,59 @@

import dev.aikido.agent_api.helpers.net.IPList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public final class IPListBuilder {
private IPListBuilder() {}

public static IPList createIPList(Collection<String> ips) {
IPList ipList = new IPList();
if (ips == null) {
return ipList; // Don't iterate over null.
return new IPList(ips);
}

public static IPList createIPListWithMappedAddresses(Collection<String> ips) {
if (ips == null || ips.isEmpty()) {
return new IPList(ips);
}

List<String> addresses = new ArrayList<>(ips);
for (String ip : ips) {
String mappedAddress = mapIPv4ToIPv6(ip);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium - Mapped-address expansion breaks legacy IPv4 allowlist and bypass entries

createIPListWithMappedAddresses now prepends ::ffff: to every colon-free entry, but shorthand IPv4 forms that this matcher still accepts as valid configuration, such as 127.1 or 10.*.*.*, are not valid embedded-IPv4 syntax inside IPv6 literals. Those synthesized entries are ignored, and the affected call sites then use plain matches(...) instead of the mapped fallback, so an incoming ::ffff: client address no longer matches the equivalent configured allowlist or bypass rule. In deployments where reverse proxies or the JVM expose clients as IPv4-mapped IPv6, this causes endpoint allowlists to deny legitimate traffic and bypass lists to stop exempting the intended clients.

Show fix

Canonicalize supported legacy IPv4 forms to a real IPv4 address/network before generating ::ffff: variants, or perform mapped-address fallback at lookup time for these call sites instead of synthesizing mapped strings from the raw configuration text.

More info - Reply on this comment to give feedback or ignore the issue.

if (mappedAddress != null) {
addresses.add(mappedAddress);
}
}
return new IPList(addresses);
}

private static String mapIPv4ToIPv6(String ip) {
if (ip == null || ip.contains(":")) {
return null;
}
for (String ip: ips) {
// Add ip address or subnet to IP list :
ipList.add(ip);

int slash = ip.indexOf('/');
if (slash < 0) {
return "::ffff:" + ip + "/128";
}

return ipList;
int prefix = 0;
int position = slash + 1;
int prefixStart = position;
while (position < ip.length()) {
char character = ip.charAt(position);
if (character < '0' || character > '9') {
break;
}
prefix = prefix * 10 + character - '0';
if (prefix > 32) {
return null;
}
position++;
}
if (position == prefixStart) {
return null;
}
return "::ffff:" + ip.substring(0, slash) + "/" + (prefix + 96);
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,32 @@
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());

@aikido-pr-checks aikido-pr-checks Bot Sep 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no-argument constructor duplicates initialization logic from the collection constructor. Delegate with this(List.of()) to keep one initialization path.

Suggested change
matcher = IPMatcher.from(List.of());
this(List.of());
Details

✨ AI Reasoning
​Both constructors initialize the same field through the same factory with an empty collection in one case. Delegating the no-argument constructor to the collection constructor would provide a single initialization path without changing behavior.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

}

public IPList(Collection<String> 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 boolean matchesWithMappedCheck(String ip) {
return matcher.matchesWithMappedCheck(ip);
}

public int length() {
return ipAddresses.size();
return matcher.size();
}
}
Loading
Loading