Skip to content

Commit 31e9c70

Browse files
committed
Update dns record collector + test cases for outbound issues
1 parent cdc06ae commit 31e9c70

3 files changed

Lines changed: 96 additions & 9 deletions

File tree

agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,24 @@ public static void report(String hostname, InetAddress[] inetAddresses) {
3535
// store stats
3636
StatisticsStore.registerCall("java.net.InetAddress.getAllByName", OperationKind.OUTGOING_HTTP_OP);
3737

38+
boolean bypassed = BypassedContextStore.isBypassed();
39+
3840
// Consume pending ports recorded by URLCollector for this hostname.
3941
// Removing them here ensures each (hostname, port) pair is counted exactly once.
4042
Set<Integer> ports = PendingHostnamesStore.getAndRemove(hostname);
41-
if (!ports.isEmpty()) {
42-
for (int port : ports) {
43-
HostnamesStore.incrementHits(hostname, port);
43+
if (!bypassed) {
44+
// Bypassed IPs are trusted — don't report their outbound hostnames in heartbeats.
45+
if (!ports.isEmpty()) {
46+
for (int port : ports) {
47+
HostnamesStore.incrementHits(hostname, port);
48+
}
49+
} else {
50+
HostnamesStore.incrementHits(hostname, 0);
4451
}
45-
} else {
46-
// We still need to report a hit to the hostname for outbound domain blocking
47-
HostnamesStore.incrementHits(hostname, 0);
4852
}
4953

5054
// Block if the hostname is in the blocked domains list
51-
if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname) && !BypassedContextStore.isBypassed()) {
55+
if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname) && !bypassed) {
5256
logger.debug("Blocking DNS lookup for domain: %s", hostname);
5357
throw BlockedOutboundException.get();
5458
}

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import dev.aikido.agent_api.storage.service_configuration.Domain;
44

5+
import java.net.IDN;
56
import java.util.HashMap;
67
import java.util.List;
8+
import java.util.Locale;
79
import java.util.Map;
810

911
public class OutboundDomains {
@@ -14,14 +16,14 @@ public void update(List<Domain> newDomains, boolean blockNewOutgoingRequests) {
1416
if (newDomains != null) {
1517
this.domains = new HashMap<>();
1618
for (Domain domain : newDomains) {
17-
this.domains.put(domain.hostname(), domain.mode());
19+
this.domains.put(normalize(domain.hostname()), domain.mode());
1820
}
1921
}
2022
this.blockNewOutgoingRequests = blockNewOutgoingRequests;
2123
}
2224

2325
public boolean shouldBlockOutgoingRequest(String hostname) {
24-
String mode = this.domains.get(hostname);
26+
String mode = this.domains.get(normalize(hostname));
2527

2628
if (this.blockNewOutgoingRequests) {
2729
// Only allow outgoing requests if the mode is "allow"
@@ -32,4 +34,18 @@ public boolean shouldBlockOutgoingRequest(String hostname) {
3234
// Only block outgoing requests if the mode is "block"
3335
return "block".equals(mode);
3436
}
37+
38+
// Normalize to lowercased Unicode form so Punycode (xn--...) and Unicode
39+
// variants of the same hostname compare equal.
40+
private static String normalize(String hostname) {
41+
if (hostname == null) {
42+
return null;
43+
}
44+
String lower = hostname.toLowerCase(Locale.ROOT);
45+
try {
46+
return IDN.toUnicode(lower, IDN.ALLOW_UNASSIGNED);
47+
} catch (IllegalArgumentException e) {
48+
return lower;
49+
}
50+
}
3551
}

agent_api/src/test/java/collectors/DNSRecordCollectorTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,73 @@ public void testBlockedDomainNotBlockedWhenIpBypassed() {
149149
);
150150
}
151151

152+
@Test
153+
public void testUnicodeDomainBlockedForPunycodeRequest() {
154+
// Blocklist stores Unicode; a Punycode request for the same domain must still be blocked.
155+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
156+
true, null, 0L, null, null, null,
157+
false, List.of(new Domain("böse.example.com", "block")), true, true, List.of()
158+
));
159+
assertThrows(BlockedOutboundException.class, () ->
160+
DNSRecordCollector.report("xn--bse-sna.example.com", new InetAddress[]{inetAddress1})
161+
);
162+
}
163+
164+
@Test
165+
public void testUnicodeDomainBlockedForUnicodeRequest() {
166+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
167+
true, null, 0L, null, null, null,
168+
false, List.of(new Domain("münchen.example.com", "block")), true, true, List.of()
169+
));
170+
assertThrows(BlockedOutboundException.class, () ->
171+
DNSRecordCollector.report("münchen.example.com", new InetAddress[]{inetAddress1})
172+
);
173+
}
174+
175+
@Test
176+
public void testAllowedUnicodeDomainNotBlockedForPunycodeRequest() {
177+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
178+
true, null, 0L, null, null, null,
179+
true, List.of(new Domain("münchen-allowed.example.com", "allow")), true, true, List.of()
180+
));
181+
assertDoesNotThrow(() ->
182+
DNSRecordCollector.report("xn--mnchen-allowed-gsb.example.com", new InetAddress[]{inetAddress1})
183+
);
184+
}
185+
186+
@Test
187+
public void testAllowedUnicodeDomainNotBlockedForUnicodeRequest() {
188+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
189+
true, null, 0L, null, null, null,
190+
true, List.of(new Domain("münchen-allowed.example.com", "allow")), true, true, List.of()
191+
));
192+
assertDoesNotThrow(() ->
193+
DNSRecordCollector.report("münchen-allowed.example.com", new InetAddress[]{inetAddress1})
194+
);
195+
}
196+
197+
@Test
198+
public void testBypassedIpDoesNotRecordHostname() {
199+
BypassedContextStore.setBypassed(true);
200+
DNSRecordCollector.report("domain1.example.com", new InetAddress[]{inetAddress1});
201+
Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList();
202+
assertEquals(0, entries.length);
203+
}
204+
205+
@Test
206+
public void testBlockedDomainStillRecordedWhenNotBypassed() {
207+
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
208+
true, null, 0L, null, null, null,
209+
false, List.of(new Domain("blocked.example.com", "block")), true, true, List.of()
210+
));
211+
assertThrows(BlockedOutboundException.class, () ->
212+
DNSRecordCollector.report("blocked.example.com", new InetAddress[]{inetAddress1})
213+
);
214+
Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList();
215+
assertEquals(1, entries.length);
216+
assertEquals("blocked.example.com", entries[0].getHostname());
217+
}
218+
152219
@Test
153220
public void testUnknownDomainBlockedWhenBlockNewOutgoingRequests() {
154221
ServiceConfigStore.updateFromAPIResponse(new APIResponse(

0 commit comments

Comments
 (0)