From 38ab7333472583f34025b2955939b24b2a7e142f Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:20:24 +0100 Subject: [PATCH 01/31] e2e: update to new core standard --- end2end/server/mock_aikido_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/end2end/server/mock_aikido_core.py b/end2end/server/mock_aikido_core.py index 7bb2cff4..6238f574 100644 --- a/end2end/server/mock_aikido_core.py +++ b/end2end/server/mock_aikido_core.py @@ -44,6 +44,8 @@ } ], "blockedUserIds": ["12345"], + "blockNewOutgoingRequests": False, + "domains": [], "block": True, }, "lists": { From 74823ae3ca26f530ec00fae31383f68494684748 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:28:09 +0100 Subject: [PATCH 02/31] Add Domain class & update APIEvent --- .../agent_api/background/cloud/api/APIResponse.java | 3 +++ .../agent_api/storage/service_configuration/Domain.java | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/APIResponse.java b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/APIResponse.java index 052e1589..07dc9f69 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/APIResponse.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/APIResponse.java @@ -1,6 +1,7 @@ package dev.aikido.agent_api.background.cloud.api; import dev.aikido.agent_api.background.Endpoint; +import dev.aikido.agent_api.storage.service_configuration.Domain; import java.util.List; @@ -11,6 +12,8 @@ public record APIResponse( List endpoints, List blockedUserIds, List allowedIPAddresses, + boolean blockNewOutgoingRequests, + List domains, boolean receivedAnyStats, boolean block ) { diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java new file mode 100644 index 00000000..ac6d5eea --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java @@ -0,0 +1,8 @@ +package dev.aikido.agent_api.storage.service_configuration; + +public record Domain(String hostname, String mode) { + public boolean isBlockingMode() { + // mode can either be "allow" or "block" + return this.mode.equals("block"); + } +} From 7d6b71d60097ec7d140241bea5b8d6af1cd77051 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:32:15 +0100 Subject: [PATCH 03/31] Store domains & block boolean in Service Config --- .../dev/aikido/agent_api/storage/ServiceConfiguration.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java index 3a7a7c3b..604f0fd6 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java @@ -4,6 +4,7 @@ import dev.aikido.agent_api.background.cloud.api.APIResponse; import dev.aikido.agent_api.background.cloud.api.ReportingApi; import dev.aikido.agent_api.helpers.net.IPList; +import dev.aikido.agent_api.storage.service_configuration.Domain; import dev.aikido.agent_api.storage.service_configuration.ParsedFirewallLists; import dev.aikido.agent_api.storage.statistics.StatisticsStore; @@ -26,6 +27,8 @@ public class ServiceConfiguration { private IPList bypassedIPs = new IPList(); private HashSet blockedUserIDs = new HashSet<>(); private List endpoints = new ArrayList<>(); + private List domains = new ArrayList<>(); + private boolean blockNewOutgoingRequests = false; public ServiceConfiguration() { this.receivedAnyStats = true; // true by default, waiting for the startup event @@ -46,6 +49,10 @@ public void updateConfig(APIResponse apiResponse) { if (apiResponse.endpoints() != null) { this.endpoints = apiResponse.endpoints(); } + if (apiResponse.domains() != null) { + this.domains = apiResponse.domains(); + } + this.blockNewOutgoingRequests = apiResponse.blockNewOutgoingRequests(); this.receivedAnyStats = apiResponse.receivedAnyStats(); } From 6dac7ed6f122b893e1d04dbf7263918d0c020630 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:38:10 +0100 Subject: [PATCH 04/31] ServiceConfig: add shouldBlockOutgoingRequests --- .../storage/ServiceConfiguration.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java index 604f0fd6..b782f92e 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java @@ -8,9 +8,7 @@ import dev.aikido.agent_api.storage.service_configuration.ParsedFirewallLists; import dev.aikido.agent_api.storage.statistics.StatisticsStore; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.*; import static dev.aikido.agent_api.helpers.IPListBuilder.createIPList; import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.isPrivateIp; @@ -27,7 +25,7 @@ public class ServiceConfiguration { private IPList bypassedIPs = new IPList(); private HashSet blockedUserIDs = new HashSet<>(); private List endpoints = new ArrayList<>(); - private List domains = new ArrayList<>(); + private Map domains = new HashMap<>(); private boolean blockNewOutgoingRequests = false; public ServiceConfiguration() { @@ -50,7 +48,12 @@ public void updateConfig(APIResponse apiResponse) { this.endpoints = apiResponse.endpoints(); } if (apiResponse.domains() != null) { - this.domains = apiResponse.domains(); + for (Domain domain : apiResponse.domains()) { + if (this.domains.get(domain.hostname()) != null) { + continue; // use first provided domain value + } + this.domains.put(domain.hostname(), domain); + } } this.blockNewOutgoingRequests = apiResponse.blockNewOutgoingRequests(); this.receivedAnyStats = apiResponse.receivedAnyStats(); @@ -134,4 +137,18 @@ public boolean isBlockedUserAgent(String userAgent) { public record BlockedResult(boolean blocked, String description) { } + + public boolean shouldBlockOutgoingRequest(String hostname) { + Domain matchingDomain = this.domains.get(hostname); + if (matchingDomain == null) { + return false; + } + + boolean isDomainBlocked = matchingDomain.isBlockingMode(); + if (this.blockNewOutgoingRequests) { + return isDomainBlocked; + } + + return isDomainBlocked; + } } From 2c23906d03de7be54093a03c24624f6b20d442d0 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:40:50 +0100 Subject: [PATCH 05/31] ServiceConfigStore: add shouldBlockOutgoingRequest --- .../dev/aikido/agent_api/storage/ServiceConfigStore.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfigStore.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfigStore.java index 41986919..472a4f72 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfigStore.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfigStore.java @@ -90,4 +90,13 @@ public static void setMiddlewareInstalled(boolean middlewareInstalled) { mutex.writeLock().unlock(); } } + + public static boolean shouldBlockOutgoingRequest(String hostname) { + mutex.readLock().lock(); + try { + return config.shouldBlockOutgoingRequest(hostname); + } finally { + mutex.readLock().unlock(); + } + } } From c41d12b9455ca3922af87dd93146c138ee4c401b Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 2 Dec 2025 02:49:58 +0100 Subject: [PATCH 06/31] URLCollector: block based on hostname --- .../collectors/DNSRecordCollector.java | 1 + .../agent_api/collectors/URLCollector.java | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index d92b8e92..fa78c4e4 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -2,6 +2,7 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.storage.Hostnames; +import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.storage.statistics.OperationKind; import dev.aikido.agent_api.storage.statistics.StatisticsStore; import dev.aikido.agent_api.vulnerabilities.Attack; diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index d612d92d..a6b5b0e3 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -2,22 +2,31 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.context.ContextObject; +import dev.aikido.agent_api.context.User; import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; +import dev.aikido.agent_api.storage.ServiceConfigStore; +import dev.aikido.agent_api.vulnerabilities.Attack; +import dev.aikido.agent_api.vulnerabilities.Vulnerabilities; +import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import java.net.URL; +import java.util.Map; +import static dev.aikido.agent_api.helpers.ShouldBlockHelper.shouldBlock; +import static dev.aikido.agent_api.helpers.StackTrace.getCurrentStackTrace; import static dev.aikido.agent_api.helpers.url.PortParser.getPortFromURL; +import static dev.aikido.agent_api.storage.AttackQueue.attackDetected; public final class URLCollector { private static final Logger logger = LogManager.getLogger(URLCollector.class); private URLCollector() {} - public static void report(URL url) { + public static void report(URL url, String operation) { if(url != null) { if (!url.getProtocol().startsWith("http")) { - return; // Non-HTTP(S) URL + return; // Non-HTTP(S) URL } logger.trace("Adding a new URL to the cache: %s", url); int port = getPortFromURL(url); @@ -25,13 +34,41 @@ public static void report(URL url) { // We store hostname and port in two places, HostnamesStore and Context. HostnamesStore is for reporting // outbound domains. Context is to have a map of hostnames with used port numbers to detect SSRF attacks. + // hostname blocking : + String hostname = url.getHost(); + if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { + ContextObject ctx = Context.get(); + + User currentUser = null; + if (ctx != null) { + currentUser = ctx.getUser(); + } + + Attack attack = new Attack( + operation, + Vulnerabilities.SSRF, + "", + "", + Map.of(), + /* payload */ hostname, + getCurrentStackTrace(), + currentUser + ); + + attackDetected(attack, ctx); + if (shouldBlock()) { + logger.debug("Blocking request to domain: %s", hostname); + throw SSRFException.get(); + } + }; + // Store (new) hostname hits - HostnamesStore.incrementHits(url.getHost(), port); + HostnamesStore.incrementHits(hostname, port); // Add to context : ContextObject context = Context.get(); if (context != null) { - context.getHostnames().add(url.getHost(), port); + context.getHostnames().add(hostname, port); Context.set(context); } } From f77d1ebe13f15f093776961631c8519bec5ff0e6 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:19:49 +0100 Subject: [PATCH 07/31] fix bug in ReportingAPiHTTP after changes --- .../aikido/agent_api/background/cloud/api/ReportingApiHTTP.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/ReportingApiHTTP.java b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/ReportingApiHTTP.java index 5f658fb1..190da623 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/ReportingApiHTTP.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/ReportingApiHTTP.java @@ -152,7 +152,7 @@ private static APIResponse getUnsuccessfulAPIResponse(String error) { return new APIResponse( false, // Success error, - 0, null, null, null, false, false // Unimportant values. + 0, null, null, null, false, null, false, false // Unimportant values. ); } } From 3ab05572c4e5725e124208b6d91c69a076d7cfff Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:20:50 +0100 Subject: [PATCH 08/31] Add string operation param to RedirectCollector --- .../collectors/RedirectCollector.java | 6 +- .../ssrf/RedirectOriginFinderTest.java | 89 ++++++++++--------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java index aca216ea..1cd48499 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java @@ -14,11 +14,11 @@ public final class RedirectCollector { private static final Logger logger = LogManager.getLogger(RedirectCollector.class); private RedirectCollector() {} - public static void report(URL origin, URL dest) { + public static void report(URL origin, URL dest, String operation) { logger.trace("Redirect detected: [Origin]<%s> -> [Destination]<%s>", origin, dest); ContextObject context = Context.get(); // Report destination URL : - URLCollector.report(dest); + URLCollector.report(dest, operation); // Add as a node : List redirectStarterNodes = context.getRedirectStartNodes(); @@ -41,4 +41,4 @@ public static void report(URL origin, URL dest) { context.addRedirectNode(starterNode); Context.set(context); // Update context. } -} \ No newline at end of file +} diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java index 1f8428d4..5b694470 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java @@ -22,15 +22,15 @@ public void setup() { @Test public void testGetRedirectOrigin() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testGetRedirectOrigin2() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2")); - RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2"), "test-op"); + RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test"), "test-op"); assertEquals(1, Context.get().getRedirectStartNodes().size()); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); @@ -43,113 +43,113 @@ public void testGetRedirectNoRedirects() { @Test public void testGetRedirectOriginNotADestination() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); assertNull(getRedirectOrigin("example.com", 443)); } @Test public void testGetRedirectOriginNotInRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); assertNull(getRedirectOrigin("example.com", 443)); } @Test public void testGetRedirectOriginMultipleRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2")); - RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test")); - RedirectCollector.report(new URL("https://hackers.com/test"), new URL("https://another.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2"), "test-op"); + RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test"), "test-op"); + RedirectCollector.report(new URL("https://hackers.com/test"), new URL("https://another.com"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testAvoidsInfiniteLoopsWithUnrelatedCyclicRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://cycle.com/a"), new URL("https://cycle.com/b")); - RedirectCollector.report(new URL("https://cycle.com/b"), new URL("https://cycle.com/c")); - RedirectCollector.report(new URL("https://cycle.com/c"), new URL("https://cycle.com/a")); // Unrelated cycle - RedirectCollector.report(new URL("https://start.com"), new URL("https://middle.com")); // Relevant redirect - RedirectCollector.report(new URL("https://middle.com"), new URL("https://end.com")); // Relevant redirect + RedirectCollector.report(new URL("https://cycle.com/a"), new URL("https://cycle.com/b"), "test-op"); + RedirectCollector.report(new URL("https://cycle.com/b"), new URL("https://cycle.com/c"), "test-op"); + RedirectCollector.report(new URL("https://cycle.com/c"), new URL("https://cycle.com/a"), "test-op"); // Unrelated cycle + RedirectCollector.report(new URL("https://start.com"), new URL("https://middle.com"), "test-op"); // Relevant redirect + RedirectCollector.report(new URL("https://middle.com"), new URL("https://end.com"), "test-op"); // Relevant redirect assertEquals("https://start.com", getRedirectOrigin("end.com", 443).toString()); } @Test public void testHandlesMultipleRequestsWithOverlappingRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://site1.com"), new URL("https://site2.com")); - RedirectCollector.report(new URL("https://site2.com"), new URL("https://site3.com")); - RedirectCollector.report(new URL("https://site3.com"), new URL("https://site1.com")); // Cycle - RedirectCollector.report(new URL("https://origin.com"), new URL("https://destination.com")); // Relevant redirect + RedirectCollector.report(new URL("https://site1.com"), new URL("https://site2.com"), "test-op"); + RedirectCollector.report(new URL("https://site2.com"), new URL("https://site3.com"), "test-op"); + RedirectCollector.report(new URL("https://site3.com"), new URL("https://site1.com"), "test-op"); // Cycle + RedirectCollector.report(new URL("https://origin.com"), new URL("https://destination.com"), "test-op"); // Relevant redirect assertEquals("https://origin.com", getRedirectOrigin("destination.com", 443).toString()); } @Test public void testAvoidsInfiniteLoopsWhenCyclesArePartOfTheRedirectChain() throws MalformedURLException { - RedirectCollector.report(new URL("https://start.com"), new URL("https://loop.com/a")); - RedirectCollector.report(new URL("https://loop.com/a"), new URL("https://loop.com/b")); - RedirectCollector.report(new URL("https://loop.com/b"), new URL("https://loop.com/c")); - RedirectCollector.report(new URL("https://loop.com/c"), new URL("https://loop.com/a")); // Cycle here + RedirectCollector.report(new URL("https://start.com"), new URL("https://loop.com/a"), "test-op"); + RedirectCollector.report(new URL("https://loop.com/a"), new URL("https://loop.com/b"), "test-op"); + RedirectCollector.report(new URL("https://loop.com/b"), new URL("https://loop.com/c"), "test-op"); + RedirectCollector.report(new URL("https://loop.com/c"), new URL("https://loop.com/a"), "test-op"); // Cycle here assertEquals("https://start.com", getRedirectOrigin("loop.com", 443).toString()); } @Test public void testRedirectsWithQueryParameters() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=value")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=value"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithFragmentIdentifiers() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com#section")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com#section"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithDifferentProtocols() throws MalformedURLException { - RedirectCollector.report(new URL("http://example.com"), new URL("https://example.com")); + RedirectCollector.report(new URL("http://example.com"), new URL("https://example.com"), "test-op"); assertEquals("http://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithDifferentPorts() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com:8080")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com:8080"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 8080).toString()); } @Test public void testRedirectsWithPaths() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/home")); - RedirectCollector.report(new URL("https://example.com/home"), new URL("https://example.com/home/welcome")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/home"), "test-op"); + RedirectCollector.report(new URL("https://example.com/home"), new URL("https://example.com/home/welcome"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testMultipleRedirectsToSameDestination() throws MalformedURLException { - RedirectCollector.report(new URL("https://a.com"), new URL("https://d.com")); - RedirectCollector.report(new URL("https://b.com"), new URL("https://d.com")); - RedirectCollector.report(new URL("https://c.com"), new URL("https://d.com")); + RedirectCollector.report(new URL("https://a.com"), new URL("https://d.com"), "test-op"); + RedirectCollector.report(new URL("https://b.com"), new URL("https://d.com"), "test-op"); + RedirectCollector.report(new URL("https://c.com"), new URL("https://d.com"), "test-op"); assertEquals("https://a.com", getRedirectOrigin("d.com", 443).toString()); } @Test public void testMultipleRedirectPathsToSameUrl() throws MalformedURLException { - RedirectCollector.report(new URL("https://x.com"), new URL("https://y.com")); - RedirectCollector.report(new URL("https://y.com"), new URL("https://z.com")); - RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com")); - RedirectCollector.report(new URL("https://b.com"), new URL("https://z.com")); + RedirectCollector.report(new URL("https://x.com"), new URL("https://y.com"), "test-op"); + RedirectCollector.report(new URL("https://y.com"), new URL("https://z.com"), "test-op"); + RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com"), "test-op"); + RedirectCollector.report(new URL("https://b.com"), new URL("https://z.com"), "test-op"); assertEquals("https://x.com", getRedirectOrigin("z.com", 443).toString()); } @Test public void testReturnsUndefinedWhenSourceAndDestinationAreSameUrl() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @@ -159,7 +159,8 @@ public void testHandlesVeryLongRedirectChains() throws MalformedURLException { for (int i = 0; i < 100; i++) { RedirectCollector.report( new URL("https://example.com/" + i), - new URL("https://example.com/" + (i + 1)) + new URL("https://example.com/" + (i + 1)), + , "test-op" ); } @@ -168,35 +169,35 @@ public void testHandlesVeryLongRedirectChains() throws MalformedURLException { @Test public void testHandlesRedirectsWithCyclesLongerThanOneRedirect() throws MalformedURLException { - RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com")); - RedirectCollector.report(new URL("https://b.com"), new URL("https://c.com")); - RedirectCollector.report(new URL("https://c.com"), new URL("https://a.com")); // Cycle + RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com"), "test-op"); + RedirectCollector.report(new URL("https://b.com"), new URL("https://c.com"), "test-op"); + RedirectCollector.report(new URL("https://c.com"), new URL("https://a.com"), "test-op"); // Cycle assertEquals("https://a.com", getRedirectOrigin("a.com", 443).toString()); } @Test public void testHandlesRedirectsWithDifferentQueryParameters() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=1")); - RedirectCollector.report(new URL("https://example.com?param=1"), new URL("https://example.com?param=2")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=1"), "test-op"); + RedirectCollector.report(new URL("https://example.com?param=1"), new URL("https://example.com?param=2"), "test-op"); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectWithMatchingPort() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:443")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:443"), "test-op"); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testRedirectWithNonMatchingPort() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:442")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:442"), "test-op"); assertNull(getRedirectOrigin("hackers.com", 443)); } @Test public void testRedirectWithNonMatchingPort2() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); assertNull(getRedirectOrigin("hackers.com", 442)); } } From c08c0071b4a57e559ac81f9e591ae6129d32dd83 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:23:11 +0100 Subject: [PATCH 09/31] OkHttp & Apache: pass along operation --- .../java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java | 2 +- .../src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java index 5b73db4b..d6db0373 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java @@ -49,7 +49,7 @@ public static void before( } if (uri != null) { // Report the URL : - URLCollector.report(uri.toURL()); + URLCollector.report(uri.toURL(), "org.apache.http.HttpClient.execute"); } } } diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java index ee4f30c6..cd9a18eb 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java @@ -50,7 +50,7 @@ public static void before( URL url = (URL) toUrlMethod.invoke(urlObject); // Report the URL - URLCollector.report(url); + URLCollector.report(url, "okhttp3.OkHttpClient.newCall"); } } } From 713685b495e656febad921a9b1b35999662d8ec1 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:24:11 +0100 Subject: [PATCH 10/31] HttpURLConnectionWrapper: report operation --- .../dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java index fde7530f..a376bd1c 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java @@ -53,7 +53,7 @@ public static void before( // Run report with "argument" for (Method method2: clazz.getMethods()) { if(method2.getName().equals("report")) { - method2.invoke(null, url); + method2.invoke(null, url, "HttpUrlConnection"); break; } } From 4d11503f2d0800c4612230e1b3b323651bd78894 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:26:48 +0100 Subject: [PATCH 11/31] fix broken test case due to two , in RedirectOriginFinderTest.java --- .../java/vulnerabilities/ssrf/RedirectOriginFinderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java index 5b694470..c804ca72 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java @@ -160,7 +160,7 @@ public void testHandlesVeryLongRedirectChains() throws MalformedURLException { RedirectCollector.report( new URL("https://example.com/" + i), new URL("https://example.com/" + (i + 1)), - , "test-op" + "test-op" ); } From 4ae3d7e9ee319968126bce732ff20febf86f9cd7 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:30:28 +0100 Subject: [PATCH 12/31] Fix test cases in ShouldBlockRequestTest --- agent_api/src/test/java/ShouldBlockRequestTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_api/src/test/java/ShouldBlockRequestTest.java b/agent_api/src/test/java/ShouldBlockRequestTest.java index a03f6294..92b3cb89 100644 --- a/agent_api/src/test/java/ShouldBlockRequestTest.java +++ b/agent_api/src/test/java/ShouldBlockRequestTest.java @@ -87,7 +87,7 @@ public void testUserSet() throws SQLException { ServiceConfigStore.updateFromAPIResponse(new APIResponse( true, "", getUnixTimeMS(), List.of(), /* blockedUserIds */ List.of("ID1", "ID2", "ID3"), List.of(), - false, true + false, null, false, true )); var res2 = ShouldBlockRequest.shouldBlockRequest(); assertTrue(res2.block()); @@ -227,7 +227,7 @@ public void testBlockedUserWithMultipleEndpoints() throws SQLException { ); List blockedUserIds = List.of("ID1"); ServiceConfigStore.updateFromAPIResponse(new APIResponse( - true, "", getUnixTimeMS(), endpoints, blockedUserIds, List.of(), true, false + true, "", getUnixTimeMS(), endpoints, blockedUserIds, List.of(), false, null,true, false )); // Call the method From 503a961866b763bbfbd6a67c1356663a141a35eb Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:32:43 +0100 Subject: [PATCH 13/31] Fix test cases for SSRFDetectorTest --- .../ssrf/SSRFDetectorTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java index 1d013535..3e94d62e 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java @@ -55,8 +55,8 @@ public void testSsrfDetectorWithRedirectTo127IP() throws MalformedURLException { // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/ssrf-test"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080")); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -79,8 +79,8 @@ public void testSsrfDetectorWithRedirectTo127IPButHostnameCapitalizationDifferen // Setup context : setContextAndLifecycle("http://Ssrf-redirects.testssandbox.com/ssrf-test"); - URLCollector.report(new URL("http://Ssrf-redirects.testssandbox.com/ssrf-test")); - RedirectCollector.report(new URL("http://ssrf-Redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080")); + URLCollector.report(new URL("http://Ssrf-redirects.testssandbox.com/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://ssrf-Redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -103,8 +103,8 @@ public void testSsrfDetectorWithRedirectToLocalhost() throws MalformedURLExcepti // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), @@ -130,8 +130,8 @@ public void testSsrfDetectorWithRedirectToLocalhostButIsRequestToItself() throws "http://ssrf-redirects.testssandbox.com/examplesite")); // url - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), @@ -147,8 +147,8 @@ public void testSsrfDetectorWithServiceHostnameInRedirect() throws MalformedURLE // Setup context : setContextAndLifecycle("http://mysql-database/ssrf-test"); - URLCollector.report(new URL("http://mysql-database/ssrf-test")); - RedirectCollector.report(new URL("http://mysql-database/ssrf-test"), new URL("http://127.0.0.1:8080")); + URLCollector.report(new URL("http://mysql-database/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://mysql-database/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -164,8 +164,8 @@ public void testSsrfDetectorForcedProtectionOff() throws MalformedURLException { // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/", "/api2/forced-off-route"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), From 6faea11ff57fcb6df5c9b2575e63e3027d711f40 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:34:20 +0100 Subject: [PATCH 14/31] Fix test cases for URLCollectorTest --- .../test/java/collectors/URLCollectorTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/agent_api/src/test/java/collectors/URLCollectorTest.java b/agent_api/src/test/java/collectors/URLCollectorTest.java index cc6551c4..c0699fa5 100644 --- a/agent_api/src/test/java/collectors/URLCollectorTest.java +++ b/agent_api/src/test/java/collectors/URLCollectorTest.java @@ -38,8 +38,8 @@ private void setContextAndLifecycle(String url) { @Test public void testNewUrlConnectionWithPort() throws IOException { setContextAndLifecycle(""); - - URLCollector.report(new URL("http://localhost:8080")); + + URLCollector.report(new URL("http://localhost:8080"), "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(8080, hostnameArray[0].getPort()); @@ -49,7 +49,7 @@ public void testNewUrlConnectionWithPort() throws IOException { @Test public void testNewUrlConnectionWithHttp() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("http://app.local.aikido.io")); + URLCollector.report(new URL("http://app.local.aikido.io"), "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(80, hostnameArray[0].getPort()); @@ -64,7 +64,7 @@ public void testNewUrlConnectionWithHttp() throws IOException { @Test public void testNewUrlConnectionHttps() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("https://aikido.dev")); + URLCollector.report(new URL("https://aikido.dev"), "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); @@ -79,7 +79,7 @@ public void testNewUrlConnectionHttps() throws IOException { @Test public void testNewUrlConnectionFaultyProtocol() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("ftp://localhost:8080")); + URLCollector.report(new URL("ftp://localhost:8080"), "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(0, hostnameArray.length); Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); @@ -89,7 +89,7 @@ public void testNewUrlConnectionFaultyProtocol() throws IOException { @Test public void testWithNullURL() throws IOException { setContextAndLifecycle(""); - URLCollector.report(null); + URLCollector.report(null, "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(0, hostnameArray.length); Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); @@ -100,7 +100,7 @@ public void testWithNullURL() throws IOException { public void testWithNullContext() throws IOException { setContextAndLifecycle(""); Context.reset(); - URLCollector.report(new URL("https://aikido.dev")); + URLCollector.report(new URL("https://aikido.dev"), "test"); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); @@ -112,10 +112,10 @@ public void testWithNullContext() throws IOException { public void testOnlyContext() throws IOException { setContextAndLifecycle(""); HostnamesStore.clear(); - URLCollector.report(new URL("https://aikido.dev")); + URLCollector.report(new URL("https://aikido.dev"), "test"); Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); assertEquals("aikido.dev", hostnameArray[0].getHostname()); } -} \ No newline at end of file +} From 4383c6eb2db6cfde159a15d13746588a4fec8d02 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 01:36:16 +0100 Subject: [PATCH 15/31] Fix test cases for WebRequestCollectorTest --- .../src/test/java/collectors/WebRequestCollectorTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent_api/src/test/java/collectors/WebRequestCollectorTest.java b/agent_api/src/test/java/collectors/WebRequestCollectorTest.java index 75999d50..b7d04e9f 100644 --- a/agent_api/src/test/java/collectors/WebRequestCollectorTest.java +++ b/agent_api/src/test/java/collectors/WebRequestCollectorTest.java @@ -212,7 +212,7 @@ void testReport_userAgentBlocked_Ip_Bypassed() { List bypassedIps = List.of("192.168.1.1"); ServiceConfigStore.updateFromAPIResponse(new APIResponse( - true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, true, false + true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, false, null, true, false )); @@ -231,7 +231,7 @@ void testReport_ipBlockedUsingLists_Ip_Bypassed() { List bypassedIps = List.of("192.168.1.1"); ServiceConfigStore.updateFromAPIResponse(new APIResponse( - true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, true, false + true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, false, null, true, false )); WebRequestCollector.Res response = WebRequestCollector.report(contextObject); @@ -251,7 +251,7 @@ void testReport_ipNotAllowedUsingLists_Ip_Bypassed() { List bypassedIps = List.of("192.168.1.1"); ServiceConfigStore.updateFromAPIResponse(new APIResponse( - true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, true, false + true, "", getUnixTimeMS(), List.of(), List.of(), bypassedIps, false, null, true, false )); From b4199a48afb7e46ebf774260029880c9b83fff9f Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 07:38:58 +0100 Subject: [PATCH 16/31] Fix ServiceConfigurationTest test cases --- .../java/storage/ServiceConfigurationTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/agent_api/src/test/java/storage/ServiceConfigurationTest.java b/agent_api/src/test/java/storage/ServiceConfigurationTest.java index c1703c30..99fd276e 100644 --- a/agent_api/src/test/java/storage/ServiceConfigurationTest.java +++ b/agent_api/src/test/java/storage/ServiceConfigurationTest.java @@ -33,6 +33,8 @@ public void testUpdateConfig() { List.of(mock(Endpoint.class)), List.of("user1", "user2"), List.of("192.168.1.1"), + false, + null, true, true ); @@ -62,6 +64,8 @@ public void testUpdateConfigWithUnsuccessfulResponse() { null, null, false, + null, + false, false ); @@ -301,6 +305,8 @@ public void testReceivedAnyStats() { null, null, false, + null, + false, true ); @@ -318,6 +324,8 @@ public void testIsIpBypassedWithEmptyBypassedList() { null, null, Collections.emptyList(), + false, + null, true, true )); @@ -334,6 +342,8 @@ public void testIsIpBypassedWithMultipleBypassedEntries() { null, null, List.of("192.168.1.1", "192.168.1.2"), + false, + null, true, true ); @@ -354,6 +364,8 @@ public void testIsUserBlockedWithEmptyBlockedUserList() { null, Collections.emptyList(), null, + false, + null, true, true )); @@ -370,6 +382,8 @@ public void testIsUserBlockedWithMultipleBlockedUsers() { null, List.of("user1", "user2"), null, + false, + null, true, true ); @@ -389,6 +403,8 @@ public void testGetEndpoints() { List.of(mock(Endpoint.class)), null, null, + false, + null, true, true ); @@ -407,6 +423,8 @@ public void testGetEndpointsWithEmptyList() { Collections.emptyList(), null, null, + false, + null, true, true ); From bf4ccd7835f5050d7f1127fffe3ec5a511943240 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 4 Dec 2025 07:46:22 +0100 Subject: [PATCH 17/31] Fix EmptyAPIResponses: add empty bool & domains --- agent_api/src/test/java/utils/EmptyAPIResponses.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_api/src/test/java/utils/EmptyAPIResponses.java b/agent_api/src/test/java/utils/EmptyAPIResponses.java index 091631a8..ea98520c 100644 --- a/agent_api/src/test/java/utils/EmptyAPIResponses.java +++ b/agent_api/src/test/java/utils/EmptyAPIResponses.java @@ -12,14 +12,14 @@ public class EmptyAPIResponses { public final static APIResponse emptyAPIResponse = new APIResponse( - true, "", UnixTimeMS.getUnixTimeMS(), List.of(), List.of(), List.of(), true, false + true, "", UnixTimeMS.getUnixTimeMS(), List.of(), List.of(), List.of(), false, null,true, false ); public final static ReportingApi.APIListsResponse emptyAPIListsResponse = new ReportingApi.APIListsResponse( List.of(), List.of(), List.of(), null, null, List.of() ); public static void setEmptyConfigWithEndpointList(List endpoints) { ServiceConfigStore.updateFromAPIResponse(new APIResponse( - true, "", getUnixTimeMS(), endpoints, List.of(), List.of(), true, false + true, "", getUnixTimeMS(), endpoints, List.of(), List.of(), false, null, true, false )); } } From 721a89115ad89642cc9b9e5c50aa040d11c0d032 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 13:11:03 +0100 Subject: [PATCH 18/31] Update ServiceConfiguration.java's shouldBlockOutgoingRequest --- .../storage/ServiceConfiguration.java | 11 +++--- .../storage/ServiceConfigurationTest.java | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java index b782f92e..6d83f47e 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java @@ -140,15 +140,14 @@ public record BlockedResult(boolean blocked, String description) { public boolean shouldBlockOutgoingRequest(String hostname) { Domain matchingDomain = this.domains.get(hostname); - if (matchingDomain == null) { - return false; - } - boolean isDomainBlocked = matchingDomain.isBlockingMode(); if (this.blockNewOutgoingRequests) { - return isDomainBlocked; + // Only allow outgoing requests if the mode is "allow" + // unknown hostnames also get blocked. + return matchingDomain == null || matchingDomain.isBlockingMode(); } - return isDomainBlocked; + // Only block outgoing requests if the mode is "block" + return matchingDomain != null && matchingDomain.isBlockingMode(); } } diff --git a/agent_api/src/test/java/storage/ServiceConfigurationTest.java b/agent_api/src/test/java/storage/ServiceConfigurationTest.java index 99fd276e..3926fc57 100644 --- a/agent_api/src/test/java/storage/ServiceConfigurationTest.java +++ b/agent_api/src/test/java/storage/ServiceConfigurationTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import dev.aikido.agent_api.storage.service_configuration.Domain; + import java.util.Collections; import java.util.List; @@ -565,6 +567,39 @@ public void testIsIpBlockedWithOnlyBlockedIPs() { assertFalse(resultNotBlocked.blocked()); } + @Test + public void testShouldBlockOutgoingRequest() { + APIResponse apiResponse = new APIResponse( + true, null, 0L, null, null, null, + true, + List.of(new Domain("example.com", "block"), new Domain("allowed.com", "allow")), + true, true + ); + serviceConfiguration.updateConfig(apiResponse); + + // blockNewOutgoingRequests=true: unknown hostname gets blocked + assertTrue(serviceConfiguration.shouldBlockOutgoingRequest("unknown.com")); + // blockNewOutgoingRequests=true: "allow" mode is not blocked + assertFalse(serviceConfiguration.shouldBlockOutgoingRequest("allowed.com")); + // blockNewOutgoingRequests=true: "block" mode is blocked + assertTrue(serviceConfiguration.shouldBlockOutgoingRequest("example.com")); + + APIResponse apiResponse2 = new APIResponse( + true, null, 0L, null, null, null, + false, + List.of(new Domain("example.com", "block"), new Domain("allowed.com", "allow")), + true, true + ); + serviceConfiguration.updateConfig(apiResponse2); + + // blockNewOutgoingRequests=false: unknown hostname is not blocked + assertFalse(serviceConfiguration.shouldBlockOutgoingRequest("unknown.com")); + // blockNewOutgoingRequests=false: "allow" mode is not blocked + assertFalse(serviceConfiguration.shouldBlockOutgoingRequest("allowed.com")); + // blockNewOutgoingRequests=false: "block" mode is blocked + assertTrue(serviceConfiguration.shouldBlockOutgoingRequest("example.com")); + } + @Test public void testIsIpBlockedWithAllowedIPsAndBlockedIPs() { ReportingApi.ListsResponseEntry allowedEntry1 = new ReportingApi.ListsResponseEntry("key", "source", "allowed", List.of("10.0.0.1")); From 78e1d3ab01ad30bf0c77b70f26fd44896cb2f64b Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 13:23:31 +0100 Subject: [PATCH 19/31] Create BlockedOutboundException --- .../aikido/agent_api/collectors/URLCollector.java | 4 ++-- .../BlockedOutboundException.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/BlockedOutboundException.java diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index a6b5b0e3..5fb139c4 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -9,7 +9,7 @@ import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.vulnerabilities.Attack; import dev.aikido.agent_api.vulnerabilities.Vulnerabilities; -import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; +import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import java.net.URL; import java.util.Map; @@ -58,7 +58,7 @@ public static void report(URL url, String operation) { attackDetected(attack, ctx); if (shouldBlock()) { logger.debug("Blocking request to domain: %s", hostname); - throw SSRFException.get(); + throw BlockedOutboundException.get(); } }; diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/BlockedOutboundException.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/BlockedOutboundException.java new file mode 100644 index 00000000..da55cb32 --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/BlockedOutboundException.java @@ -0,0 +1,14 @@ +package dev.aikido.agent_api.vulnerabilities.outbound_blocking; + +import dev.aikido.agent_api.vulnerabilities.AikidoException; + +public class BlockedOutboundException extends AikidoException { + public BlockedOutboundException(String msg) { + super(msg); + } + + public static BlockedOutboundException get() { + String defaultMsg = generateDefaultMessage("an outbound request"); + return new BlockedOutboundException(defaultMsg); + } +} From 1f20a918d3d8da117cf98297bb9ab74d600fa3ac Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 13:23:50 +0100 Subject: [PATCH 20/31] Create a new OutboundDomains.java class fixing issues with prev impl --- .../storage/ServiceConfiguration.java | 26 +++----------- .../outbound_blocking/OutboundDomains.java | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java index 6d83f47e..d6f4597d 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/ServiceConfiguration.java @@ -4,8 +4,8 @@ import dev.aikido.agent_api.background.cloud.api.APIResponse; import dev.aikido.agent_api.background.cloud.api.ReportingApi; import dev.aikido.agent_api.helpers.net.IPList; -import dev.aikido.agent_api.storage.service_configuration.Domain; import dev.aikido.agent_api.storage.service_configuration.ParsedFirewallLists; +import dev.aikido.agent_api.vulnerabilities.outbound_blocking.OutboundDomains; import dev.aikido.agent_api.storage.statistics.StatisticsStore; import java.util.*; @@ -25,8 +25,7 @@ public class ServiceConfiguration { private IPList bypassedIPs = new IPList(); private HashSet blockedUserIDs = new HashSet<>(); private List endpoints = new ArrayList<>(); - private Map domains = new HashMap<>(); - private boolean blockNewOutgoingRequests = false; + private OutboundDomains outboundDomains = new OutboundDomains(); public ServiceConfiguration() { this.receivedAnyStats = true; // true by default, waiting for the startup event @@ -47,15 +46,7 @@ public void updateConfig(APIResponse apiResponse) { if (apiResponse.endpoints() != null) { this.endpoints = apiResponse.endpoints(); } - if (apiResponse.domains() != null) { - for (Domain domain : apiResponse.domains()) { - if (this.domains.get(domain.hostname()) != null) { - continue; // use first provided domain value - } - this.domains.put(domain.hostname(), domain); - } - } - this.blockNewOutgoingRequests = apiResponse.blockNewOutgoingRequests(); + this.outboundDomains.update(apiResponse.domains(), apiResponse.blockNewOutgoingRequests()); this.receivedAnyStats = apiResponse.receivedAnyStats(); } @@ -139,15 +130,6 @@ public record BlockedResult(boolean blocked, String description) { } public boolean shouldBlockOutgoingRequest(String hostname) { - Domain matchingDomain = this.domains.get(hostname); - - if (this.blockNewOutgoingRequests) { - // Only allow outgoing requests if the mode is "allow" - // unknown hostnames also get blocked. - return matchingDomain == null || matchingDomain.isBlockingMode(); - } - - // Only block outgoing requests if the mode is "block" - return matchingDomain != null && matchingDomain.isBlockingMode(); + return this.outboundDomains.shouldBlockOutgoingRequest(hostname); } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java new file mode 100644 index 00000000..7b230148 --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java @@ -0,0 +1,35 @@ +package dev.aikido.agent_api.vulnerabilities.outbound_blocking; + +import dev.aikido.agent_api.storage.service_configuration.Domain; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OutboundDomains { + private Map domains = new HashMap<>(); + private boolean blockNewOutgoingRequests = false; + + public void update(List newDomains, boolean blockNewOutgoingRequests) { + if (newDomains != null) { + this.domains = new HashMap<>(); + for (Domain domain : newDomains) { + this.domains.putIfAbsent(domain.hostname(), domain); + } + } + this.blockNewOutgoingRequests = blockNewOutgoingRequests; + } + + public boolean shouldBlockOutgoingRequest(String hostname) { + Domain matchingDomain = this.domains.get(hostname); + + if (this.blockNewOutgoingRequests) { + // Only allow outgoing requests if the mode is "allow" + // null means unknown hostname, so they get blocked + return matchingDomain == null || matchingDomain.isBlockingMode(); + } + + // Only block outgoing requests if the mode is "block" + return matchingDomain != null && matchingDomain.isBlockingMode(); + } +} From ee001fa4771ca5ce8cdd0aa56ee4340d94972af1 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 13:31:11 +0100 Subject: [PATCH 21/31] just store a string-string map in OutboundDomains --- .../storage/service_configuration/Domain.java | 4 ---- .../outbound_blocking/OutboundDomains.java | 10 +++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java index ac6d5eea..bbc8c4ad 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/Domain.java @@ -1,8 +1,4 @@ package dev.aikido.agent_api.storage.service_configuration; public record Domain(String hostname, String mode) { - public boolean isBlockingMode() { - // mode can either be "allow" or "block" - return this.mode.equals("block"); - } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java index 7b230148..84e8bc28 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/outbound_blocking/OutboundDomains.java @@ -7,29 +7,29 @@ import java.util.Map; public class OutboundDomains { - private Map domains = new HashMap<>(); + private Map domains = new HashMap<>(); private boolean blockNewOutgoingRequests = false; public void update(List newDomains, boolean blockNewOutgoingRequests) { if (newDomains != null) { this.domains = new HashMap<>(); for (Domain domain : newDomains) { - this.domains.putIfAbsent(domain.hostname(), domain); + this.domains.put(domain.hostname(), domain.mode()); } } this.blockNewOutgoingRequests = blockNewOutgoingRequests; } public boolean shouldBlockOutgoingRequest(String hostname) { - Domain matchingDomain = this.domains.get(hostname); + String mode = this.domains.get(hostname); if (this.blockNewOutgoingRequests) { // Only allow outgoing requests if the mode is "allow" // null means unknown hostname, so they get blocked - return matchingDomain == null || matchingDomain.isBlockingMode(); + return !"allow".equals(mode); } // Only block outgoing requests if the mode is "block" - return matchingDomain != null && matchingDomain.isBlockingMode(); + return "block".equals(mode); } } From a8bc36a641efb5f515fbdb878ce42a99e19e9e6e Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 13:44:34 +0100 Subject: [PATCH 22/31] Cleanup URLCollector & perform simplified check in DNSRecordCollector also adds test cases for DNSRecordCollector --- .../collectors/DNSRecordCollector.java | 9 ++++- .../agent_api/collectors/URLCollector.java | 33 --------------- .../collectors/DNSRecordCollectorTest.java | 40 +++++++++++++++++++ 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index fa78c4e4..739ec850 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -7,6 +7,7 @@ import dev.aikido.agent_api.storage.statistics.StatisticsStore; import dev.aikido.agent_api.vulnerabilities.Attack; import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFDetector; +import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; @@ -31,6 +32,12 @@ public static void report(String hostname, InetAddress[] inetAddresses) { // store stats StatisticsStore.registerCall("java.net.InetAddress.getAllByName", OperationKind.OUTGOING_HTTP_OP); + // Block if the hostname is in the blocked domains list + if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { + logger.debug("Blocking DNS lookup for domain: %s", hostname); + throw BlockedOutboundException.get(); + } + // Convert inetAddresses array to a List of IP strings : List ipAddresses = new ArrayList<>(); for (InetAddress inetAddress : inetAddresses) { @@ -77,7 +84,7 @@ public static void report(String hostname, InetAddress[] inetAddresses) { } } - } catch (SSRFException | StoredSSRFException e) { + } catch (BlockedOutboundException | SSRFException | StoredSSRFException e) { throw e; } catch (Throwable e) { logger.trace(e); diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index 5fb139c4..b6865b77 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -2,22 +2,15 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.context.ContextObject; -import dev.aikido.agent_api.context.User; import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; import dev.aikido.agent_api.storage.ServiceConfigStore; -import dev.aikido.agent_api.vulnerabilities.Attack; -import dev.aikido.agent_api.vulnerabilities.Vulnerabilities; import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import java.net.URL; -import java.util.Map; -import static dev.aikido.agent_api.helpers.ShouldBlockHelper.shouldBlock; -import static dev.aikido.agent_api.helpers.StackTrace.getCurrentStackTrace; import static dev.aikido.agent_api.helpers.url.PortParser.getPortFromURL; -import static dev.aikido.agent_api.storage.AttackQueue.attackDetected; public final class URLCollector { private static final Logger logger = LogManager.getLogger(URLCollector.class); @@ -34,33 +27,7 @@ public static void report(URL url, String operation) { // We store hostname and port in two places, HostnamesStore and Context. HostnamesStore is for reporting // outbound domains. Context is to have a map of hostnames with used port numbers to detect SSRF attacks. - // hostname blocking : String hostname = url.getHost(); - if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { - ContextObject ctx = Context.get(); - - User currentUser = null; - if (ctx != null) { - currentUser = ctx.getUser(); - } - - Attack attack = new Attack( - operation, - Vulnerabilities.SSRF, - "", - "", - Map.of(), - /* payload */ hostname, - getCurrentStackTrace(), - currentUser - ); - - attackDetected(attack, ctx); - if (shouldBlock()) { - logger.debug("Blocking request to domain: %s", hostname); - throw BlockedOutboundException.get(); - } - }; // Store (new) hostname hits HostnamesStore.incrementHits(hostname, port); diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index 95814a3d..a9672c55 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -1,5 +1,6 @@ package collectors; +import dev.aikido.agent_api.background.cloud.api.APIResponse; import dev.aikido.agent_api.background.cloud.api.events.DetectedAttack; import dev.aikido.agent_api.collectors.DNSRecordCollector; import dev.aikido.agent_api.context.Context; @@ -8,7 +9,9 @@ import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; +import dev.aikido.agent_api.storage.service_configuration.Domain; import dev.aikido.agent_api.vulnerabilities.Attack; +import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import dev.aikido.agent_api.vulnerabilities.ssrf.StoredSSRFException; import org.junit.jupiter.api.*; @@ -40,6 +43,10 @@ public void cleanup() { HostnamesStore.clear(); Context.set(null); AttackQueue.clear(); + // Reset domain config + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, false, List.of(), true, false + )); } @Test @@ -135,6 +142,39 @@ public void testHostnameSameWithContextAsAStoredSSRFAttack() { }); } + @Test + public void testBlockedDomain() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + false, List.of(new Domain("blocked.example.com", "block")), true, true + )); + assertThrows(BlockedOutboundException.class, () -> + DNSRecordCollector.report("blocked.example.com", new InetAddress[]{inetAddress1}) + ); + } + + @Test + public void testAllowedDomainNotBlocked() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + false, List.of(new Domain("allowed.example.com", "allow")), true, true + )); + assertDoesNotThrow(() -> + DNSRecordCollector.report("allowed.example.com", new InetAddress[]{inetAddress1}) + ); + } + + @Test + public void testUnknownDomainBlockedWhenBlockNewOutgoingRequests() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + true, List.of(), true, true + )); + assertThrows(BlockedOutboundException.class, () -> + DNSRecordCollector.report("unknown.example.com", new InetAddress[]{inetAddress1}) + ); + } + @Test public void testStoredSSRFWithNoContext() throws InterruptedException { ServiceConfigStore.updateBlocking(true); From 2507368b2bdcd26ef35b39a055a24287b1bf6a98 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 14:10:17 +0100 Subject: [PATCH 23/31] Cleanup operation pass along --- .../collectors/RedirectCollector.java | 6 +- .../agent_api/collectors/URLCollector.java | 4 +- .../java/collectors/URLCollectorTest.java | 18 ++-- .../ssrf/RedirectOriginFinderTest.java | 89 +++++++++---------- 4 files changed, 57 insertions(+), 60 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java index 1cd48499..aca216ea 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/RedirectCollector.java @@ -14,11 +14,11 @@ public final class RedirectCollector { private static final Logger logger = LogManager.getLogger(RedirectCollector.class); private RedirectCollector() {} - public static void report(URL origin, URL dest, String operation) { + public static void report(URL origin, URL dest) { logger.trace("Redirect detected: [Origin]<%s> -> [Destination]<%s>", origin, dest); ContextObject context = Context.get(); // Report destination URL : - URLCollector.report(dest, operation); + URLCollector.report(dest); // Add as a node : List redirectStarterNodes = context.getRedirectStartNodes(); @@ -41,4 +41,4 @@ public static void report(URL origin, URL dest, String operation) { context.addRedirectNode(starterNode); Context.set(context); // Update context. } -} +} \ No newline at end of file diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index b6865b77..39cba62f 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -5,8 +5,6 @@ import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; -import dev.aikido.agent_api.storage.ServiceConfigStore; -import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import java.net.URL; @@ -16,7 +14,7 @@ public final class URLCollector { private static final Logger logger = LogManager.getLogger(URLCollector.class); private URLCollector() {} - public static void report(URL url, String operation) { + public static void report(URL url) { if(url != null) { if (!url.getProtocol().startsWith("http")) { return; // Non-HTTP(S) URL diff --git a/agent_api/src/test/java/collectors/URLCollectorTest.java b/agent_api/src/test/java/collectors/URLCollectorTest.java index c0699fa5..cc6551c4 100644 --- a/agent_api/src/test/java/collectors/URLCollectorTest.java +++ b/agent_api/src/test/java/collectors/URLCollectorTest.java @@ -38,8 +38,8 @@ private void setContextAndLifecycle(String url) { @Test public void testNewUrlConnectionWithPort() throws IOException { setContextAndLifecycle(""); - - URLCollector.report(new URL("http://localhost:8080"), "test"); + + URLCollector.report(new URL("http://localhost:8080")); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(8080, hostnameArray[0].getPort()); @@ -49,7 +49,7 @@ public void testNewUrlConnectionWithPort() throws IOException { @Test public void testNewUrlConnectionWithHttp() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("http://app.local.aikido.io"), "test"); + URLCollector.report(new URL("http://app.local.aikido.io")); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(80, hostnameArray[0].getPort()); @@ -64,7 +64,7 @@ public void testNewUrlConnectionWithHttp() throws IOException { @Test public void testNewUrlConnectionHttps() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("https://aikido.dev"), "test"); + URLCollector.report(new URL("https://aikido.dev")); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); @@ -79,7 +79,7 @@ public void testNewUrlConnectionHttps() throws IOException { @Test public void testNewUrlConnectionFaultyProtocol() throws IOException { setContextAndLifecycle(""); - URLCollector.report(new URL("ftp://localhost:8080"), "test"); + URLCollector.report(new URL("ftp://localhost:8080")); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(0, hostnameArray.length); Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); @@ -89,7 +89,7 @@ public void testNewUrlConnectionFaultyProtocol() throws IOException { @Test public void testWithNullURL() throws IOException { setContextAndLifecycle(""); - URLCollector.report(null, "test"); + URLCollector.report(null); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(0, hostnameArray.length); Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); @@ -100,7 +100,7 @@ public void testWithNullURL() throws IOException { public void testWithNullContext() throws IOException { setContextAndLifecycle(""); Context.reset(); - URLCollector.report(new URL("https://aikido.dev"), "test"); + URLCollector.report(new URL("https://aikido.dev")); Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); @@ -112,10 +112,10 @@ public void testWithNullContext() throws IOException { public void testOnlyContext() throws IOException { setContextAndLifecycle(""); HostnamesStore.clear(); - URLCollector.report(new URL("https://aikido.dev"), "test"); + URLCollector.report(new URL("https://aikido.dev")); Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); assertEquals("aikido.dev", hostnameArray[0].getHostname()); } -} +} \ No newline at end of file diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java index c804ca72..1f8428d4 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/RedirectOriginFinderTest.java @@ -22,15 +22,15 @@ public void setup() { @Test public void testGetRedirectOrigin() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testGetRedirectOrigin2() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2"), "test-op"); - RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2")); + RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test")); assertEquals(1, Context.get().getRedirectStartNodes().size()); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); @@ -43,113 +43,113 @@ public void testGetRedirectNoRedirects() { @Test public void testGetRedirectOriginNotADestination() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); assertNull(getRedirectOrigin("example.com", 443)); } @Test public void testGetRedirectOriginNotInRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); assertNull(getRedirectOrigin("example.com", 443)); } @Test public void testGetRedirectOriginMultipleRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2"), "test-op"); - RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test"), "test-op"); - RedirectCollector.report(new URL("https://hackers.com/test"), new URL("https://another.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/2")); + RedirectCollector.report(new URL("https://example.com/2"), new URL("https://hackers.com/test")); + RedirectCollector.report(new URL("https://hackers.com/test"), new URL("https://another.com")); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testAvoidsInfiniteLoopsWithUnrelatedCyclicRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://cycle.com/a"), new URL("https://cycle.com/b"), "test-op"); - RedirectCollector.report(new URL("https://cycle.com/b"), new URL("https://cycle.com/c"), "test-op"); - RedirectCollector.report(new URL("https://cycle.com/c"), new URL("https://cycle.com/a"), "test-op"); // Unrelated cycle - RedirectCollector.report(new URL("https://start.com"), new URL("https://middle.com"), "test-op"); // Relevant redirect - RedirectCollector.report(new URL("https://middle.com"), new URL("https://end.com"), "test-op"); // Relevant redirect + RedirectCollector.report(new URL("https://cycle.com/a"), new URL("https://cycle.com/b")); + RedirectCollector.report(new URL("https://cycle.com/b"), new URL("https://cycle.com/c")); + RedirectCollector.report(new URL("https://cycle.com/c"), new URL("https://cycle.com/a")); // Unrelated cycle + RedirectCollector.report(new URL("https://start.com"), new URL("https://middle.com")); // Relevant redirect + RedirectCollector.report(new URL("https://middle.com"), new URL("https://end.com")); // Relevant redirect assertEquals("https://start.com", getRedirectOrigin("end.com", 443).toString()); } @Test public void testHandlesMultipleRequestsWithOverlappingRedirects() throws MalformedURLException { - RedirectCollector.report(new URL("https://site1.com"), new URL("https://site2.com"), "test-op"); - RedirectCollector.report(new URL("https://site2.com"), new URL("https://site3.com"), "test-op"); - RedirectCollector.report(new URL("https://site3.com"), new URL("https://site1.com"), "test-op"); // Cycle - RedirectCollector.report(new URL("https://origin.com"), new URL("https://destination.com"), "test-op"); // Relevant redirect + RedirectCollector.report(new URL("https://site1.com"), new URL("https://site2.com")); + RedirectCollector.report(new URL("https://site2.com"), new URL("https://site3.com")); + RedirectCollector.report(new URL("https://site3.com"), new URL("https://site1.com")); // Cycle + RedirectCollector.report(new URL("https://origin.com"), new URL("https://destination.com")); // Relevant redirect assertEquals("https://origin.com", getRedirectOrigin("destination.com", 443).toString()); } @Test public void testAvoidsInfiniteLoopsWhenCyclesArePartOfTheRedirectChain() throws MalformedURLException { - RedirectCollector.report(new URL("https://start.com"), new URL("https://loop.com/a"), "test-op"); - RedirectCollector.report(new URL("https://loop.com/a"), new URL("https://loop.com/b"), "test-op"); - RedirectCollector.report(new URL("https://loop.com/b"), new URL("https://loop.com/c"), "test-op"); - RedirectCollector.report(new URL("https://loop.com/c"), new URL("https://loop.com/a"), "test-op"); // Cycle here + RedirectCollector.report(new URL("https://start.com"), new URL("https://loop.com/a")); + RedirectCollector.report(new URL("https://loop.com/a"), new URL("https://loop.com/b")); + RedirectCollector.report(new URL("https://loop.com/b"), new URL("https://loop.com/c")); + RedirectCollector.report(new URL("https://loop.com/c"), new URL("https://loop.com/a")); // Cycle here assertEquals("https://start.com", getRedirectOrigin("loop.com", 443).toString()); } @Test public void testRedirectsWithQueryParameters() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=value"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=value")); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithFragmentIdentifiers() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com#section"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com#section")); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithDifferentProtocols() throws MalformedURLException { - RedirectCollector.report(new URL("http://example.com"), new URL("https://example.com"), "test-op"); + RedirectCollector.report(new URL("http://example.com"), new URL("https://example.com")); assertEquals("http://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectsWithDifferentPorts() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com:8080"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com:8080")); assertEquals("https://example.com", getRedirectOrigin("example.com", 8080).toString()); } @Test public void testRedirectsWithPaths() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/home"), "test-op"); - RedirectCollector.report(new URL("https://example.com/home"), new URL("https://example.com/home/welcome"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com/home")); + RedirectCollector.report(new URL("https://example.com/home"), new URL("https://example.com/home/welcome")); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testMultipleRedirectsToSameDestination() throws MalformedURLException { - RedirectCollector.report(new URL("https://a.com"), new URL("https://d.com"), "test-op"); - RedirectCollector.report(new URL("https://b.com"), new URL("https://d.com"), "test-op"); - RedirectCollector.report(new URL("https://c.com"), new URL("https://d.com"), "test-op"); + RedirectCollector.report(new URL("https://a.com"), new URL("https://d.com")); + RedirectCollector.report(new URL("https://b.com"), new URL("https://d.com")); + RedirectCollector.report(new URL("https://c.com"), new URL("https://d.com")); assertEquals("https://a.com", getRedirectOrigin("d.com", 443).toString()); } @Test public void testMultipleRedirectPathsToSameUrl() throws MalformedURLException { - RedirectCollector.report(new URL("https://x.com"), new URL("https://y.com"), "test-op"); - RedirectCollector.report(new URL("https://y.com"), new URL("https://z.com"), "test-op"); - RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com"), "test-op"); - RedirectCollector.report(new URL("https://b.com"), new URL("https://z.com"), "test-op"); + RedirectCollector.report(new URL("https://x.com"), new URL("https://y.com")); + RedirectCollector.report(new URL("https://y.com"), new URL("https://z.com")); + RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com")); + RedirectCollector.report(new URL("https://b.com"), new URL("https://z.com")); assertEquals("https://x.com", getRedirectOrigin("z.com", 443).toString()); } @Test public void testReturnsUndefinedWhenSourceAndDestinationAreSameUrl() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com")); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @@ -159,8 +159,7 @@ public void testHandlesVeryLongRedirectChains() throws MalformedURLException { for (int i = 0; i < 100; i++) { RedirectCollector.report( new URL("https://example.com/" + i), - new URL("https://example.com/" + (i + 1)), - "test-op" + new URL("https://example.com/" + (i + 1)) ); } @@ -169,35 +168,35 @@ public void testHandlesVeryLongRedirectChains() throws MalformedURLException { @Test public void testHandlesRedirectsWithCyclesLongerThanOneRedirect() throws MalformedURLException { - RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com"), "test-op"); - RedirectCollector.report(new URL("https://b.com"), new URL("https://c.com"), "test-op"); - RedirectCollector.report(new URL("https://c.com"), new URL("https://a.com"), "test-op"); // Cycle + RedirectCollector.report(new URL("https://a.com"), new URL("https://b.com")); + RedirectCollector.report(new URL("https://b.com"), new URL("https://c.com")); + RedirectCollector.report(new URL("https://c.com"), new URL("https://a.com")); // Cycle assertEquals("https://a.com", getRedirectOrigin("a.com", 443).toString()); } @Test public void testHandlesRedirectsWithDifferentQueryParameters() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=1"), "test-op"); - RedirectCollector.report(new URL("https://example.com?param=1"), new URL("https://example.com?param=2"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://example.com?param=1")); + RedirectCollector.report(new URL("https://example.com?param=1"), new URL("https://example.com?param=2")); assertEquals("https://example.com", getRedirectOrigin("example.com", 443).toString()); } @Test public void testRedirectWithMatchingPort() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:443"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:443")); assertNotNull(getRedirectOrigin("hackers.com", 443)); assertEquals("https://example.com", getRedirectOrigin("hackers.com", 443).toString()); } @Test public void testRedirectWithNonMatchingPort() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:442"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com:442")); assertNull(getRedirectOrigin("hackers.com", 443)); } @Test public void testRedirectWithNonMatchingPort2() throws MalformedURLException { - RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com"), "test-op"); + RedirectCollector.report(new URL("https://example.com"), new URL("https://hackers.com")); assertNull(getRedirectOrigin("hackers.com", 442)); } } From a6905cef8a03635ef96df43c3a875f173b485258 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 14:12:19 +0100 Subject: [PATCH 24/31] Fix ApacheHttpClientWrapper.java: revert passing operation --- .../java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java index d6db0373..5b73db4b 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/ApacheHttpClientWrapper.java @@ -49,7 +49,7 @@ public static void before( } if (uri != null) { // Report the URL : - URLCollector.report(uri.toURL(), "org.apache.http.HttpClient.execute"); + URLCollector.report(uri.toURL()); } } } From ad1e941ed8ccb610958319beccb4550c3b0c67ad Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 14:15:20 +0100 Subject: [PATCH 25/31] Last reverts to not pass operation anymore to the URLCollector --- .../wrappers/HttpURLConnectionWrapper.java | 2 +- .../aikido/agent/wrappers/OkHttpWrapper.java | 2 +- .../ssrf/SSRFDetectorTest.java | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java index a376bd1c..fde7530f 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/HttpURLConnectionWrapper.java @@ -53,7 +53,7 @@ public static void before( // Run report with "argument" for (Method method2: clazz.getMethods()) { if(method2.getName().equals("report")) { - method2.invoke(null, url, "HttpUrlConnection"); + method2.invoke(null, url); break; } } diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java index cd9a18eb..ee4f30c6 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/OkHttpWrapper.java @@ -50,7 +50,7 @@ public static void before( URL url = (URL) toUrlMethod.invoke(urlObject); // Report the URL - URLCollector.report(url, "okhttp3.OkHttpClient.newCall"); + URLCollector.report(url); } } } diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java index 3e94d62e..1d013535 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java @@ -55,8 +55,8 @@ public void testSsrfDetectorWithRedirectTo127IP() throws MalformedURLException { // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/ssrf-test"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080")); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -79,8 +79,8 @@ public void testSsrfDetectorWithRedirectTo127IPButHostnameCapitalizationDifferen // Setup context : setContextAndLifecycle("http://Ssrf-redirects.testssandbox.com/ssrf-test"); - URLCollector.report(new URL("http://Ssrf-redirects.testssandbox.com/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://ssrf-Redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); + URLCollector.report(new URL("http://Ssrf-redirects.testssandbox.com/ssrf-test")); + RedirectCollector.report(new URL("http://ssrf-Redirects.testssandbox.com/ssrf-test"), new URL("http://127.0.0.1:8080")); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -103,8 +103,8 @@ public void testSsrfDetectorWithRedirectToLocalhost() throws MalformedURLExcepti // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), @@ -130,8 +130,8 @@ public void testSsrfDetectorWithRedirectToLocalhostButIsRequestToItself() throws "http://ssrf-redirects.testssandbox.com/examplesite")); // url - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), @@ -147,8 +147,8 @@ public void testSsrfDetectorWithServiceHostnameInRedirect() throws MalformedURLE // Setup context : setContextAndLifecycle("http://mysql-database/ssrf-test"); - URLCollector.report(new URL("http://mysql-database/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://mysql-database/ssrf-test"), new URL("http://127.0.0.1:8080"), "test"); + URLCollector.report(new URL("http://mysql-database/ssrf-test")); + RedirectCollector.report(new URL("http://mysql-database/ssrf-test"), new URL("http://127.0.0.1:8080")); Attack attackData = SSRFDetector.run( "127.0.0.1", 8080, List.of("127.0.0.1"), @@ -164,8 +164,8 @@ public void testSsrfDetectorForcedProtectionOff() throws MalformedURLException { // Setup context : setContextAndLifecycle("http://ssrf-redirects.testssandbox.com/", "/api2/forced-off-route"); - URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), "test"); - RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost"), "test"); + URLCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test")); + RedirectCollector.report(new URL("http://ssrf-redirects.testssandbox.com/ssrf-test"), new URL("http://localhost")); Attack attackData = SSRFDetector.run( "localhost", 80, List.of("127.0.0.1"), From 6018a4514645c7da1717ee534c344c137ce47091 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 14:32:38 +0100 Subject: [PATCH 26/31] Add some extra test cases --- .../java/storage/ServiceConfigStoreTest.java | 61 ++++++++++ .../OutboundDomainsTest.java | 109 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 agent_api/src/test/java/storage/ServiceConfigStoreTest.java create mode 100644 agent_api/src/test/java/vulnerabilities/outbound_blocking/OutboundDomainsTest.java diff --git a/agent_api/src/test/java/storage/ServiceConfigStoreTest.java b/agent_api/src/test/java/storage/ServiceConfigStoreTest.java new file mode 100644 index 00000000..acb11df9 --- /dev/null +++ b/agent_api/src/test/java/storage/ServiceConfigStoreTest.java @@ -0,0 +1,61 @@ +package storage; + +import dev.aikido.agent_api.background.cloud.api.APIResponse; +import dev.aikido.agent_api.storage.ServiceConfigStore; +import dev.aikido.agent_api.storage.service_configuration.Domain; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ServiceConfigStoreTest { + + @BeforeEach + public void setUp() { + // Reset to a known state: no domains, blockNewOutgoingRequests=false + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + false, null, true, false + )); + } + + @Test + public void testShouldBlockOutgoingRequestNotBlockedByDefault() { + assertFalse(ServiceConfigStore.shouldBlockOutgoingRequest("example.com")); + } + + @Test + public void testShouldBlockOutgoingRequestBlockedDomain() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + false, + List.of(new Domain("blocked.com", "block")), + true, false + )); + assertTrue(ServiceConfigStore.shouldBlockOutgoingRequest("blocked.com")); + } + + @Test + public void testShouldBlockOutgoingRequestAllowedDomain() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + true, + List.of(new Domain("allowed.com", "allow")), + true, false + )); + assertFalse(ServiceConfigStore.shouldBlockOutgoingRequest("allowed.com")); + } + + @Test + public void testShouldBlockOutgoingRequestUnknownWhenBlockNewEnabled() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, + true, + List.of(new Domain("allowed.com", "allow")), + true, false + )); + assertTrue(ServiceConfigStore.shouldBlockOutgoingRequest("unknown.com")); + } +} diff --git a/agent_api/src/test/java/vulnerabilities/outbound_blocking/OutboundDomainsTest.java b/agent_api/src/test/java/vulnerabilities/outbound_blocking/OutboundDomainsTest.java new file mode 100644 index 00000000..45590dbd --- /dev/null +++ b/agent_api/src/test/java/vulnerabilities/outbound_blocking/OutboundDomainsTest.java @@ -0,0 +1,109 @@ +package vulnerabilities.outbound_blocking; + +import dev.aikido.agent_api.storage.service_configuration.Domain; +import dev.aikido.agent_api.vulnerabilities.outbound_blocking.OutboundDomains; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OutboundDomainsTest { + + private OutboundDomains outboundDomains; + + @BeforeEach + public void setUp() { + outboundDomains = new OutboundDomains(); + } + + // --- shouldBlockOutgoingRequest with blockNewOutgoingRequests=false (default) --- + + @Test + public void testDefaultDoesNotBlockUnknownHostname() { + assertFalse(outboundDomains.shouldBlockOutgoingRequest("unknown.com")); + } + + @Test + public void testBlockModeBlocksHostname() { + outboundDomains.update(List.of(new Domain("blocked.com", "block")), false); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("blocked.com")); + } + + @Test + public void testAllowModeDoesNotBlockHostname() { + outboundDomains.update(List.of(new Domain("allowed.com", "allow")), false); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("allowed.com")); + } + + @Test + public void testUnknownHostnameNotBlockedWhenBlockNewOutgoingRequestsFalse() { + outboundDomains.update(List.of(new Domain("blocked.com", "block")), false); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("unknown.com")); + } + + // --- shouldBlockOutgoingRequest with blockNewOutgoingRequests=true --- + + @Test + public void testUnknownHostnameBlockedWhenBlockNewOutgoingRequestsTrue() { + outboundDomains.update(List.of(), true); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("unknown.com")); + } + + @Test + public void testAllowModeNotBlockedWhenBlockNewOutgoingRequestsTrue() { + outboundDomains.update(List.of(new Domain("allowed.com", "allow")), true); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("allowed.com")); + } + + @Test + public void testBlockModeBlockedWhenBlockNewOutgoingRequestsTrue() { + outboundDomains.update(List.of(new Domain("blocked.com", "block")), true); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("blocked.com")); + } + + // --- update() behaviour --- + + @Test + public void testUpdateWithNullDomainsPreservesExistingDomains() { + outboundDomains.update(List.of(new Domain("blocked.com", "block")), false); + // null domains should not reset the map + outboundDomains.update(null, false); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("blocked.com")); + } + + @Test + public void testUpdateWithNullDomainsUpdatesBlockFlag() { + outboundDomains.update(null, true); + // blockNewOutgoingRequests should now be true even though domains unchanged + assertTrue(outboundDomains.shouldBlockOutgoingRequest("unknown.com")); + } + + @Test + public void testUpdateReplacesDomainsMap() { + outboundDomains.update(List.of(new Domain("old.com", "block")), false); + outboundDomains.update(List.of(new Domain("new.com", "block")), false); + // old entry should be gone + assertFalse(outboundDomains.shouldBlockOutgoingRequest("old.com")); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("new.com")); + } + + @Test + public void testUpdateWithEmptyListClearsDomainsMap() { + outboundDomains.update(List.of(new Domain("blocked.com", "block")), false); + outboundDomains.update(List.of(), false); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("blocked.com")); + } + + @Test + public void testMultipleDomainsWithMixedModes() { + outboundDomains.update(List.of( + new Domain("blocked.com", "block"), + new Domain("allowed.com", "allow") + ), false); + assertTrue(outboundDomains.shouldBlockOutgoingRequest("blocked.com")); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("allowed.com")); + assertFalse(outboundDomains.shouldBlockOutgoingRequest("other.com")); + } +} From 012d6923d3e643ed49c3a92014d7840d3b5b4d8e Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 15:07:31 +0100 Subject: [PATCH 27/31] DNSRecordCollector: Do Hostname reporting now as well --- .../collectors/DNSRecordCollector.java | 67 ++++++++++------- .../agent_api/collectors/URLCollector.java | 10 +-- .../collectors/DNSRecordCollectorTest.java | 72 +++++++++++++++++++ .../java/collectors/URLCollectorTest.java | 23 ++---- 4 files changed, 121 insertions(+), 51 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index 739ec850..cade7941 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -2,6 +2,7 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.storage.Hostnames; +import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.storage.statistics.OperationKind; import dev.aikido.agent_api.storage.statistics.StatisticsStore; @@ -32,6 +33,17 @@ public static void report(String hostname, InetAddress[] inetAddresses) { // store stats StatisticsStore.registerCall("java.net.InetAddress.getAllByName", OperationKind.OUTGOING_HTTP_OP); + List portsFromContext = getPortsFromContext(hostname); + if (!portsFromContext.isEmpty()) { + for (int port : portsFromContext) { + HostnamesStore.incrementHits(hostname, port); + } + } else { + // ensure that even if URLCollector didn't receive this hostname it still gets reported to core + // so that we can be confident in our outbound domain blocking + HostnamesStore.incrementHits(hostname, 0); + } + // Block if the hostname is in the blocked domains list if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { logger.debug("Blocking DNS lookup for domain: %s", hostname); @@ -44,32 +56,25 @@ public static void report(String hostname, InetAddress[] inetAddresses) { ipAddresses.add(inetAddress.getHostAddress()); } - // Fetch hostnames from Context (this is to get port number e.g.) - if (Context.get() != null && Context.get().getHostnames() != null) { - for (Hostnames.HostnameEntry hostnameEntry : Context.get().getHostnames().asArray()) { - if (!hostnameEntry.getHostname().equals(hostname)) { - continue; - } - logger.debug("Hostname: %s, Port: %s, IPs: %s", hostnameEntry.getHostname(), hostnameEntry.getPort(), ipAddresses); - - Attack attack = SSRFDetector.run( - hostname, hostnameEntry.getPort(), ipAddresses, OPERATION_NAME - ); - if (attack == null) { - continue; - } - - logger.debug("SSRF Attack detected due to: %s:%s", hostname, hostnameEntry.getPort()); - attackDetected(attack, Context.get()); - - if (shouldBlock()) { - logger.debug("Blocking SSRF attack..."); - throw SSRFException.get(); - } - - // We don't want to test for a stored SSRF attack. - return; + // Run SSRF check for all ports found for this hostname in context + for (int port : portsFromContext) { + logger.debug("Hostname: %s, Port: %s, IPs: %s", hostname, port, ipAddresses); + + Attack attack = SSRFDetector.run(hostname, port, ipAddresses, OPERATION_NAME); + if (attack == null) { + continue; + } + + logger.debug("SSRF Attack detected due to: %s:%s", hostname, port); + attackDetected(attack, Context.get()); + + if (shouldBlock()) { + logger.debug("Blocking SSRF attack..."); + throw SSRFException.get(); } + + // We don't want to test for a stored SSRF attack. + return; } // We don't need the context object to check for stored ssrf, but we do want to run this after our other @@ -90,4 +95,16 @@ public static void report(String hostname, InetAddress[] inetAddresses) { logger.trace(e); } } + + private static List getPortsFromContext(String hostname) { + List ports = new ArrayList<>(); + if (Context.get() != null && Context.get().getHostnames() != null) { + for (Hostnames.HostnameEntry hostnameEntry : Context.get().getHostnames().asArray()) { + if (hostnameEntry.getHostname().equals(hostname)) { + ports.add(hostnameEntry.getPort()); + } + } + } + return ports; + } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index 39cba62f..43c22c1f 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -2,7 +2,6 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.context.ContextObject; -import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; @@ -21,16 +20,9 @@ public static void report(URL url) { } logger.trace("Adding a new URL to the cache: %s", url); int port = getPortFromURL(url); - - // We store hostname and port in two places, HostnamesStore and Context. HostnamesStore is for reporting - // outbound domains. Context is to have a map of hostnames with used port numbers to detect SSRF attacks. - String hostname = url.getHost(); - // Store (new) hostname hits - HostnamesStore.incrementHits(hostname, port); - - // Add to context : + // Add hostname and port to context so DNSRecordCollector can use it for SSRF detection and outbound domains ContextObject context = Context.get(); if (context != null) { context.getHostnames().add(hostname, port); diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index a9672c55..ce104b30 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -175,6 +175,78 @@ public void testUnknownDomainBlockedWhenBlockNewOutgoingRequests() { ); } + @Test + public void testHostnamesStorePort0WhenNoContext() { + Context.set(null); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("dev.aikido", entries[0].getHostname()); + assertEquals(0, entries[0].getPort()); + } + + @Test + public void testHostnamesStorePort0WhenHostnameNotInContext() { + ContextObject ctx = mock(ContextObject.class); + Hostnames hostnames = new Hostnames(20); + hostnames.add("other.hostname", 8080); + when(ctx.getHostnames()).thenReturn(hostnames); + Context.set(ctx); + + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("dev.aikido", entries[0].getHostname()); + assertEquals(0, entries[0].getPort()); + } + + @Test + public void testHostnamesStoreUsesPortFromContext() { + ContextObject ctx = mock(ContextObject.class); + Hostnames hostnames = new Hostnames(20); + hostnames.add("dev.aikido", 8080); + when(ctx.getHostnames()).thenReturn(hostnames); + Context.set(ctx); + + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("dev.aikido", entries[0].getHostname()); + assertEquals(8080, entries[0].getPort()); + } + + @Test + public void testHostnamesStoreIncrementedForAllPortsFromContext() { + ContextObject ctx = mock(ContextObject.class); + Hostnames hostnames = new Hostnames(20); + hostnames.add("dev.aikido", 80); + hostnames.add("dev.aikido", 443); + when(ctx.getHostnames()).thenReturn(hostnames); + Context.set(ctx); + + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(2, entries.length); + assertEquals(80, entries[0].getPort()); + assertEquals(443, entries[1].getPort()); + } + + @Test + public void testSSRFStillRunsWhenPortInContextIsZero() { + ServiceConfigStore.updateBlocking(true); + + ContextObject myContextObject = new SampleContextObject(); + myContextObject.getHostnames().add("dev.aikido", 0); + Context.set(myContextObject); + + Exception exception = assertThrows(SSRFException.class, () -> { + DNSRecordCollector.report("dev.aikido", new InetAddress[]{ + inetAddress1, inetAddress2 + }); + }); + assertEquals("Aikido Zen has blocked a server-side request forgery", exception.getMessage()); + } + @Test public void testStoredSSRFWithNoContext() throws InterruptedException { ServiceConfigStore.updateBlocking(true); diff --git a/agent_api/src/test/java/collectors/URLCollectorTest.java b/agent_api/src/test/java/collectors/URLCollectorTest.java index cc6551c4..34cc413b 100644 --- a/agent_api/src/test/java/collectors/URLCollectorTest.java +++ b/agent_api/src/test/java/collectors/URLCollectorTest.java @@ -38,9 +38,9 @@ private void setContextAndLifecycle(String url) { @Test public void testNewUrlConnectionWithPort() throws IOException { setContextAndLifecycle(""); - + URLCollector.report(new URL("http://localhost:8080")); - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); + Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); assertEquals(1, hostnameArray.length); assertEquals(8080, hostnameArray[0].getPort()); assertEquals("localhost", hostnameArray[0].getHostname()); @@ -50,30 +50,20 @@ public void testNewUrlConnectionWithPort() throws IOException { public void testNewUrlConnectionWithHttp() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("http://app.local.aikido.io")); - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); + Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); assertEquals(1, hostnameArray.length); assertEquals(80, hostnameArray[0].getPort()); assertEquals("app.local.aikido.io", hostnameArray[0].getHostname()); - - Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray2.length); - assertEquals(80, hostnameArray2[0].getPort()); - assertEquals("app.local.aikido.io", hostnameArray2[0].getHostname()); } @Test public void testNewUrlConnectionHttps() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("https://aikido.dev")); - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); + Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); assertEquals(1, hostnameArray.length); assertEquals(443, hostnameArray[0].getPort()); assertEquals("aikido.dev", hostnameArray[0].getHostname()); - - Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray2.length); - assertEquals(443, hostnameArray2[0].getPort()); - assertEquals("aikido.dev", hostnameArray2[0].getHostname()); } @Test @@ -101,10 +91,9 @@ public void testWithNullContext() throws IOException { setContextAndLifecycle(""); Context.reset(); URLCollector.report(new URL("https://aikido.dev")); + // URLCollector only writes to context, so HostnamesStore stays empty when context is null Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); - assertEquals(1, hostnameArray.length); - assertEquals(443, hostnameArray[0].getPort()); - assertEquals("aikido.dev", hostnameArray[0].getHostname()); + assertEquals(0, hostnameArray.length); assertNull(Context.get()); } From ba9a0cfe720b9615cda600b87dcc7b89e6e1ac4d Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 15:40:32 +0100 Subject: [PATCH 28/31] Create a PendingHostnamesStore for URL <-> DNS Bridge --- .../collectors/DNSRecordCollector.java | 30 ++-- .../agent_api/collectors/URLCollector.java | 17 +- .../dev/aikido/agent_api/context/Context.java | 5 + .../agent_api/context/ContextObject.java | 6 - .../storage/PendingHostnamesStore.java | 43 +++++ .../collectors/DNSRecordCollectorTest.java | 168 ++++++------------ .../java/collectors/URLCollectorTest.java | 61 +++---- 7 files changed, 150 insertions(+), 180 deletions(-) create mode 100644 agent_api/src/main/java/dev/aikido/agent_api/storage/PendingHostnamesStore.java diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index cade7941..d33c165c 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -1,8 +1,8 @@ package dev.aikido.agent_api.collectors; import dev.aikido.agent_api.context.Context; -import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.storage.statistics.OperationKind; import dev.aikido.agent_api.storage.statistics.StatisticsStore; @@ -18,6 +18,7 @@ import java.net.InetAddress; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static dev.aikido.agent_api.helpers.ShouldBlockHelper.shouldBlock; import static dev.aikido.agent_api.storage.AttackQueue.attackDetected; @@ -33,14 +34,15 @@ public static void report(String hostname, InetAddress[] inetAddresses) { // store stats StatisticsStore.registerCall("java.net.InetAddress.getAllByName", OperationKind.OUTGOING_HTTP_OP); - List portsFromContext = getPortsFromContext(hostname); - if (!portsFromContext.isEmpty()) { - for (int port : portsFromContext) { + // Consume pending ports recorded by URLCollector for this hostname. + // Removing them here ensures each (hostname, port) pair is counted exactly once. + Set ports = PendingHostnamesStore.getAndRemove(hostname); + if (!ports.isEmpty()) { + for (int port : ports) { HostnamesStore.incrementHits(hostname, port); } } else { - // ensure that even if URLCollector didn't receive this hostname it still gets reported to core - // so that we can be confident in our outbound domain blocking + // We still need to report a hit to the hostname for outbound domain blocking HostnamesStore.incrementHits(hostname, 0); } @@ -56,8 +58,8 @@ public static void report(String hostname, InetAddress[] inetAddresses) { ipAddresses.add(inetAddress.getHostAddress()); } - // Run SSRF check for all ports found for this hostname in context - for (int port : portsFromContext) { + // Run SSRF check for all ports found in the pending store (empty = no SSRF check) + for (int port : ports) { logger.debug("Hostname: %s, Port: %s, IPs: %s", hostname, port, ipAddresses); Attack attack = SSRFDetector.run(hostname, port, ipAddresses, OPERATION_NAME); @@ -95,16 +97,4 @@ public static void report(String hostname, InetAddress[] inetAddresses) { logger.trace(e); } } - - private static List getPortsFromContext(String hostname) { - List ports = new ArrayList<>(); - if (Context.get() != null && Context.get().getHostnames() != null) { - for (Hostnames.HostnameEntry hostnameEntry : Context.get().getHostnames().asArray()) { - if (hostnameEntry.getHostname().equals(hostname)) { - ports.add(hostnameEntry.getPort()); - } - } - } - return ports; - } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java index 43c22c1f..e1c1244b 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/URLCollector.java @@ -1,9 +1,8 @@ package dev.aikido.agent_api.collectors; -import dev.aikido.agent_api.context.Context; -import dev.aikido.agent_api.context.ContextObject; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import java.net.URL; @@ -14,20 +13,14 @@ public final class URLCollector { private URLCollector() {} public static void report(URL url) { - if(url != null) { + if (url != null) { if (!url.getProtocol().startsWith("http")) { return; // Non-HTTP(S) URL } logger.trace("Adding a new URL to the cache: %s", url); - int port = getPortFromURL(url); - String hostname = url.getHost(); - - // Add hostname and port to context so DNSRecordCollector can use it for SSRF detection and outbound domains - ContextObject context = Context.get(); - if (context != null) { - context.getHostnames().add(hostname, port); - Context.set(context); - } + // Store hostname+port in the pending store so DNSRecordCollector can pick it + // up during the DNS lookup that follows, for SSRF detection and outbound hostnames + PendingHostnamesStore.add(url.getHost(), getPortFromURL(url)); } } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java b/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java index 7f5dbc7c..27977354 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java @@ -1,5 +1,7 @@ package dev.aikido.agent_api.context; +import dev.aikido.agent_api.storage.PendingHostnamesStore; + public final class Context { private Context() {} @@ -8,6 +10,9 @@ public static ContextObject get() { return threadLocalContext.get(); } public static void set(ContextObject contextObject) { + // Flush pending hostnames on every context change to prevent the store from + // growing unboundedly when a thread is reused across multiple requests. + PendingHostnamesStore.clear(); threadLocalContext.set(contextObject); } public static void reset() { diff --git a/agent_api/src/main/java/dev/aikido/agent_api/context/ContextObject.java b/agent_api/src/main/java/dev/aikido/agent_api/context/ContextObject.java index 7f84d595..33935838 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/context/ContextObject.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/context/ContextObject.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.RedirectNode; import java.util.*; @@ -26,10 +25,6 @@ public class ContextObject { protected transient Map> cache = new HashMap<>(); protected transient Optional forcedProtectionOff = Optional.empty(); - // We store hostnames in the context object so we can match a given hostname (by DNS request) - // with its port number (which we know by instrumenting the URLs that get requested). - protected transient Hostnames hostnames = new Hostnames(1000); // max 1000 entries - public boolean middlewareExecuted() {return executedMiddleware; } public void setExecutedMiddleware(boolean value) { executedMiddleware = value; } @@ -97,7 +92,6 @@ public HashMap> getCookies() { return cookies; } public Map> getCache() { return cache; } - public Hostnames getHostnames() { return hostnames; } public void setForcedProtectionOff(boolean forcedProtectionOff) { this.forcedProtectionOff = Optional.of(forcedProtectionOff); diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/PendingHostnamesStore.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/PendingHostnamesStore.java new file mode 100644 index 00000000..2efd5ecf --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/PendingHostnamesStore.java @@ -0,0 +1,43 @@ +package dev.aikido.agent_api.storage; + +import java.util.*; + +/** + * Thread-local bridge between URLCollector and DNSRecordCollector. + * URLCollector records hostname+port here; DNSRecordCollector reads and removes the entry + * so each (hostname, port) pair is processed exactly once per DNS lookup. + */ +public final class PendingHostnamesStore { + private PendingHostnamesStore() {} + + private static final ThreadLocal>> store = + ThreadLocal.withInitial(LinkedHashMap::new); + + public static void add(String hostname, int port) { + Map> map = store.get(); + if (!map.containsKey(hostname)) { + map.put(hostname, new LinkedHashSet<>()); + } + map.get(hostname).add(port); + } + + public static Set getAndRemove(String hostname) { + Set ports = store.get().remove(hostname); + if (ports == null) { + return Collections.emptySet(); + } + return ports; + } + + public static Set getPorts(String hostname) { + Set ports = store.get().get(hostname); + if (ports == null) { + return Collections.emptySet(); + } + return Collections.unmodifiableSet(ports); + } + + public static void clear() { + store.get().clear(); + } +} diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index ce104b30..e33676dd 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -8,9 +8,9 @@ import dev.aikido.agent_api.storage.AttackQueue; import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.storage.service_configuration.Domain; -import dev.aikido.agent_api.vulnerabilities.Attack; import dev.aikido.agent_api.vulnerabilities.outbound_blocking.BlockedOutboundException; import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import dev.aikido.agent_api.vulnerabilities.ssrf.StoredSSRFException; @@ -31,16 +31,18 @@ public class DNSRecordCollectorTest { @BeforeEach void setup() throws UnknownHostException { - // We want to define InetAddresses here so it does not interfere with counts of getHostname() inetAddress1 = InetAddress.getByName("1.1.1.1"); inetAddress2 = InetAddress.getByName("127.0.0.1"); imdsAddress1 = InetAddress.getByName("169.254.169.254"); AttackQueue.clear(); + HostnamesStore.clear(); + PendingHostnamesStore.clear(); } @AfterEach public void cleanup() { HostnamesStore.clear(); + PendingHostnamesStore.clear(); Context.set(null); AttackQueue.clear(); // Reset domain config @@ -49,50 +51,6 @@ public void cleanup() { )); } - @Test - public void testContextNull() { - // Early return because of Context being null : - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); - } - - @Test - public void testThreadCacheHostnames() { - ContextObject myContextObject = mock(ContextObject.class); - Context.set(myContextObject); - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); - verify(myContextObject).getHostnames(); - - myContextObject = mock(ContextObject.class); - Hostnames hostnames = new Hostnames(20); - when(myContextObject.getHostnames()).thenReturn(hostnames); - - Context.set(myContextObject); - - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); - verify(myContextObject, times(2)).getHostnames(); - } - - @Test - public void testHostnameSame() { - ContextObject myContextObject = mock(ContextObject.class); - Hostnames hostnames = new Hostnames(20); - hostnames.add("dev.aikido.not", 80); - hostnames.add("dev.aikido", 80); - when(myContextObject.getHostnames()).thenReturn(hostnames); - - Context.set(myContextObject); - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); - verify(myContextObject, times(2)).getHostnames(); - } - public static class SampleContextObject extends EmptySampleContextObject { public SampleContextObject() { super(); @@ -102,18 +60,37 @@ public SampleContextObject() { } @Test - public void testHostnameSameWithContextAsAttack() { + public void testNoPendingHostnames() { + // No pending hostnames → port 0 recorded, no SSRF check + Context.set(new EmptySampleContextObject()); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1, inetAddress2}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("dev.aikido", entries[0].getHostname()); + assertEquals(0, entries[0].getPort()); + } + + @Test + public void testPendingHostnameOtherThanLookedUp() { + // A pending entry for a different hostname should not affect the looked-up hostname + PendingHostnamesStore.add("dev.aikido.not", 80); + Context.set(new EmptySampleContextObject()); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1, inetAddress2}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("dev.aikido", entries[0].getHostname()); + assertEquals(0, entries[0].getPort()); + } + + @Test + public void testSSRFWithPendingHostname() { ServiceConfigStore.updateBlocking(true); - ContextObject myContextObject = new SampleContextObject(); - myContextObject.getHostnames().add("dev.aikido.not", 80); - myContextObject.getHostnames().add("dev.aikido", 80); - Context.set(myContextObject); + PendingHostnamesStore.add("dev.aikido", 80); + Context.set(new SampleContextObject()); Exception exception = assertThrows(SSRFException.class, () -> { - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1, inetAddress2}); }); assertEquals("Aikido Zen has blocked a server-side request forgery", exception.getMessage()); } @@ -122,23 +99,16 @@ public void testHostnameSameWithContextAsAttack() { public void testHostnameSameWithContextAsAStoredSSRFAttack() { ServiceConfigStore.updateBlocking(true); - ContextObject myContextObject = new SampleContextObject(); - Context.set(myContextObject); + Context.set(new SampleContextObject()); Exception exception = assertThrows(StoredSSRFException.class, () -> { - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{imdsAddress1, inetAddress2}); }); assertEquals("Aikido Zen has blocked a stored server-side request forgery", exception.getMessage()); assertDoesNotThrow(() -> { - DNSRecordCollector.report("metadata.goog", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); - DNSRecordCollector.report("metadata.google.internal", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); + DNSRecordCollector.report("metadata.goog", new InetAddress[]{imdsAddress1, inetAddress2}); + DNSRecordCollector.report("metadata.google.internal", new InetAddress[]{imdsAddress1, inetAddress2}); }); } @@ -176,7 +146,7 @@ public void testUnknownDomainBlockedWhenBlockNewOutgoingRequests() { } @Test - public void testHostnamesStorePort0WhenNoContext() { + public void testHostnamesStorePort0WhenNoPendingEntry() { Context.set(null); DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); @@ -186,63 +156,49 @@ public void testHostnamesStorePort0WhenNoContext() { } @Test - public void testHostnamesStorePort0WhenHostnameNotInContext() { - ContextObject ctx = mock(ContextObject.class); - Hostnames hostnames = new Hostnames(20); - hostnames.add("other.hostname", 8080); - when(ctx.getHostnames()).thenReturn(hostnames); - Context.set(ctx); + public void testHostnamesStoreUsesPortFromPendingStore() { + PendingHostnamesStore.add("dev.aikido", 8080); + Context.set(mock(ContextObject.class)); DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); assertEquals(1, entries.length); assertEquals("dev.aikido", entries[0].getHostname()); - assertEquals(0, entries[0].getPort()); + assertEquals(8080, entries[0].getPort()); } @Test - public void testHostnamesStoreUsesPortFromContext() { - ContextObject ctx = mock(ContextObject.class); - Hostnames hostnames = new Hostnames(20); - hostnames.add("dev.aikido", 8080); - when(ctx.getHostnames()).thenReturn(hostnames); - Context.set(ctx); + public void testHostnamesStoreIncrementedForAllPendingPorts() { + PendingHostnamesStore.add("dev.aikido", 80); + PendingHostnamesStore.add("dev.aikido", 443); + Context.set(mock(ContextObject.class)); DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); - assertEquals(1, entries.length); - assertEquals("dev.aikido", entries[0].getHostname()); - assertEquals(8080, entries[0].getPort()); + assertEquals(2, entries.length); + assertEquals(80, entries[0].getPort()); + assertEquals(443, entries[1].getPort()); } @Test - public void testHostnamesStoreIncrementedForAllPortsFromContext() { - ContextObject ctx = mock(ContextObject.class); - Hostnames hostnames = new Hostnames(20); - hostnames.add("dev.aikido", 80); - hostnames.add("dev.aikido", 443); - when(ctx.getHostnames()).thenReturn(hostnames); - Context.set(ctx); + public void testPendingEntryRemovedAfterDNSLookup() { + PendingHostnamesStore.add("dev.aikido", 8080); + Context.set(mock(ContextObject.class)); DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1}); - Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); - assertEquals(2, entries.length); - assertEquals(80, entries[0].getPort()); - assertEquals(443, entries[1].getPort()); + // Entry should have been consumed + assertTrue(PendingHostnamesStore.getPorts("dev.aikido").isEmpty()); } @Test - public void testSSRFStillRunsWhenPortInContextIsZero() { + public void testSSRFStillRunsWhenPendingPortIsZero() { ServiceConfigStore.updateBlocking(true); - ContextObject myContextObject = new SampleContextObject(); - myContextObject.getHostnames().add("dev.aikido", 0); - Context.set(myContextObject); + PendingHostnamesStore.add("dev.aikido", 0); + Context.set(new SampleContextObject()); Exception exception = assertThrows(SSRFException.class, () -> { - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - inetAddress1, inetAddress2 - }); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{inetAddress1, inetAddress2}); }); assertEquals("Aikido Zen has blocked a server-side request forgery", exception.getMessage()); } @@ -254,9 +210,7 @@ public void testStoredSSRFWithNoContext() throws InterruptedException { Context.set(null); Exception exception = assertThrows(StoredSSRFException.class, () -> { - DNSRecordCollector.report("dev.aikido", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); + DNSRecordCollector.report("dev.aikido", new InetAddress[]{imdsAddress1, inetAddress2}); }); DetectedAttack.DetectedAttackEvent event = (DetectedAttack.DetectedAttackEvent) AttackQueue.get(); assertEquals("stored_ssrf", event.attack().kind()); @@ -265,12 +219,8 @@ public void testStoredSSRFWithNoContext() throws InterruptedException { assertEquals("Aikido Zen has blocked a stored server-side request forgery", exception.getMessage()); assertDoesNotThrow(() -> { - DNSRecordCollector.report("metadata.goog", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); - DNSRecordCollector.report("metadata.google.internal", new InetAddress[]{ - imdsAddress1, inetAddress2 - }); + DNSRecordCollector.report("metadata.goog", new InetAddress[]{imdsAddress1, inetAddress2}); + DNSRecordCollector.report("metadata.google.internal", new InetAddress[]{imdsAddress1, inetAddress2}); }); } } diff --git a/agent_api/src/test/java/collectors/URLCollectorTest.java b/agent_api/src/test/java/collectors/URLCollectorTest.java index 34cc413b..140065a3 100644 --- a/agent_api/src/test/java/collectors/URLCollectorTest.java +++ b/agent_api/src/test/java/collectors/URLCollectorTest.java @@ -2,8 +2,8 @@ import dev.aikido.agent_api.collectors.URLCollector; import dev.aikido.agent_api.context.Context; -import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -12,9 +12,9 @@ import java.io.IOException; import java.net.URL; +import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class URLCollectorTest { @BeforeAll @@ -29,6 +29,7 @@ static void afterAll() { @BeforeEach void beforeEach() { cleanup(); + PendingHostnamesStore.clear(); } private void setContextAndLifecycle(String url) { @@ -40,50 +41,43 @@ public void testNewUrlConnectionWithPort() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("http://localhost:8080")); - Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray.length); - assertEquals(8080, hostnameArray[0].getPort()); - assertEquals("localhost", hostnameArray[0].getHostname()); + Set ports = PendingHostnamesStore.getPorts("localhost"); + assertEquals(1, ports.size()); + assertTrue(ports.contains(8080)); } @Test public void testNewUrlConnectionWithHttp() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("http://app.local.aikido.io")); - Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray.length); - assertEquals(80, hostnameArray[0].getPort()); - assertEquals("app.local.aikido.io", hostnameArray[0].getHostname()); + Set ports = PendingHostnamesStore.getPorts("app.local.aikido.io"); + assertEquals(1, ports.size()); + assertTrue(ports.contains(80)); } @Test public void testNewUrlConnectionHttps() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("https://aikido.dev")); - Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray.length); - assertEquals(443, hostnameArray[0].getPort()); - assertEquals("aikido.dev", hostnameArray[0].getHostname()); + Set ports = PendingHostnamesStore.getPorts("aikido.dev"); + assertEquals(1, ports.size()); + assertTrue(ports.contains(443)); } @Test public void testNewUrlConnectionFaultyProtocol() throws IOException { setContextAndLifecycle(""); URLCollector.report(new URL("ftp://localhost:8080")); - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); - assertEquals(0, hostnameArray.length); - Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); - assertEquals(0, hostnameArray2.length); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + assertTrue(PendingHostnamesStore.getPorts("localhost").isEmpty()); } @Test public void testWithNullURL() throws IOException { setContextAndLifecycle(""); URLCollector.report(null); - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); - assertEquals(0, hostnameArray.length); - Hostnames.HostnameEntry[] hostnameArray2 = Context.get().getHostnames().asArray(); - assertEquals(0, hostnameArray2.length); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + assertTrue(PendingHostnamesStore.getPorts("localhost").isEmpty()); } @Test @@ -91,20 +85,21 @@ public void testWithNullContext() throws IOException { setContextAndLifecycle(""); Context.reset(); URLCollector.report(new URL("https://aikido.dev")); - // URLCollector only writes to context, so HostnamesStore stays empty when context is null - Hostnames.HostnameEntry[] hostnameArray = HostnamesStore.getHostnamesAsList(); - assertEquals(0, hostnameArray.length); + // URLCollector writes to PendingHostnamesStore regardless of context state + Set ports = PendingHostnamesStore.getPorts("aikido.dev"); + assertEquals(1, ports.size()); + assertTrue(ports.contains(443)); assertNull(Context.get()); } @Test - public void testOnlyContext() throws IOException { + public void testOnlyPendingStore() throws IOException { setContextAndLifecycle(""); - HostnamesStore.clear(); URLCollector.report(new URL("https://aikido.dev")); - Hostnames.HostnameEntry[] hostnameArray = Context.get().getHostnames().asArray(); - assertEquals(1, hostnameArray.length); - assertEquals(443, hostnameArray[0].getPort()); - assertEquals("aikido.dev", hostnameArray[0].getHostname()); + // HostnamesStore is only written by DNSRecordCollector, not URLCollector + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + Set ports = PendingHostnamesStore.getPorts("aikido.dev"); + assertEquals(1, ports.size()); + assertTrue(ports.contains(443)); } -} \ No newline at end of file +} From 8781eaae2470a78e6bde5cae57fd3d0e6e042765 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 15:59:04 +0100 Subject: [PATCH 29/31] Fix bug: Move clear to WebRequestCollector The clear in the Context.set was clearing this bridge and breaking redirect protectiosn --- .../agent_api/collectors/WebRequestCollector.java | 10 +++++++++- .../java/dev/aikido/agent_api/context/Context.java | 3 --- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java index d331965f..1d66a212 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java @@ -8,6 +8,7 @@ import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; import dev.aikido.agent_api.storage.AttackQueue; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; import dev.aikido.agent_api.storage.ServiceConfiguration; import dev.aikido.agent_api.storage.attack_wave_detector.AttackWaveDetectorStore; @@ -36,7 +37,14 @@ private WebRequestCollector() { */ public static Res report(ContextObject newContext) { ServiceConfiguration config = getConfig(); - Context.reset(); // clear context + + // clear context + Context.reset(); + + // Flush pending hostnames on every context change to prevent the store from + // growing unboundedly when a thread is reused across multiple requests. + PendingHostnamesStore.clear(); + if (config.isIpBypassed(newContext.getRemoteAddress())) { return null; // do not set context when the IP address is bypassed (zen = off) } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java b/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java index 27977354..0389cc90 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/context/Context.java @@ -10,9 +10,6 @@ public static ContextObject get() { return threadLocalContext.get(); } public static void set(ContextObject contextObject) { - // Flush pending hostnames on every context change to prevent the store from - // growing unboundedly when a thread is reused across multiple requests. - PendingHostnamesStore.clear(); threadLocalContext.set(contextObject); } public static void reset() { From ef695f478b26949fa1856bb2a7053d541428eda8 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Thu, 5 Mar 2026 16:15:05 +0100 Subject: [PATCH 30/31] Cleanup on HttpURLConnectionTest --- agent_api/src/test/java/wrappers/HttpURLConnectionTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent_api/src/test/java/wrappers/HttpURLConnectionTest.java b/agent_api/src/test/java/wrappers/HttpURLConnectionTest.java index ae85b395..2037f291 100644 --- a/agent_api/src/test/java/wrappers/HttpURLConnectionTest.java +++ b/agent_api/src/test/java/wrappers/HttpURLConnectionTest.java @@ -3,6 +3,7 @@ import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; +import dev.aikido.agent_api.storage.PendingHostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -25,12 +26,14 @@ public class HttpURLConnectionTest { void cleanup() { Context.set(null); HostnamesStore.clear(); + PendingHostnamesStore.clear(); } @BeforeEach void beforeEach() { cleanup(); ServiceConfigStore.updateBlocking(true); + PendingHostnamesStore.clear(); } private void setContextAndLifecycle(String url) { From 1fb079ccf091695ea7c7710a8ff1d59918690c5b Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 6 Mar 2026 15:18:48 +0100 Subject: [PATCH 31/31] Also add app.local.aikido.io to /etc/hosts --- .github/workflows/gradle-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle-tests.yml b/.github/workflows/gradle-tests.yml index 613e2460..bd90c398 100644 --- a/.github/workflows/gradle-tests.yml +++ b/.github/workflows/gradle-tests.yml @@ -59,6 +59,7 @@ jobs: - name: Add local.aikido.io to /etc/hosts run: | echo "127.0.0.1 local.aikido.io" | sudo tee -a /etc/hosts + echo "127.0.0.1 app.local.aikido.io" | sudo tee -a /etc/hosts - name: Start databases working-directory: ./sample-apps/databases