-
Notifications
You must be signed in to change notification settings - Fork 4
Add outbound blocking #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
38ab733
74823ae
7d6b71d
6dac7ed
2c23906
c41d12b
f77d1eb
3ab0557
c08c007
713685b
4d11503
4ae3d7e
503a961
6faea11
4383c6e
b4199a4
bf4ccd7
0514a0b
721a891
78e1d3a
1f20a91
ee001fa
a8bc36a
2507368
a6905ce
ad1e941
6018a45
012d692
ba9a0cf
8781eaa
ef695f4
1fb079c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| 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; | ||
| 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; | ||
|
|
@@ -15,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; | ||
|
|
@@ -30,38 +34,49 @@ public static void report(String hostname, InetAddress[] inetAddresses) { | |
| // store stats | ||
| StatisticsStore.registerCall("java.net.InetAddress.getAllByName", OperationKind.OUTGOING_HTTP_OP); | ||
|
|
||
| // Consume pending ports recorded by URLCollector for this hostname. | ||
| // Removing them here ensures each (hostname, port) pair is counted exactly once. | ||
| Set<Integer> ports = PendingHostnamesStore.getAndRemove(hostname); | ||
| if (!ports.isEmpty()) { | ||
| for (int port : ports) { | ||
| HostnamesStore.incrementHits(hostname, port); | ||
| } | ||
| } else { | ||
| // We still need to report a hit to the hostname for outbound domain blocking | ||
| HostnamesStore.incrementHits(hostname, 0); | ||
| } | ||
|
|
||
| // Block if the hostname is in the blocked domains list | ||
| if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DNSRecordCollector.report now performs enforcement (may throw BlockedOutboundException) in addition to reporting. Rename or document the method to reflect enforcement, or move blocking logic to a separate method. Detailsβ¨ AI Reasoning π§ How do I fix it? Reply |
||
| logger.debug("Blocking DNS lookup for domain: %s", hostname); | ||
| throw BlockedOutboundException.get(); | ||
| } | ||
|
|
||
| // Convert inetAddresses array to a List of IP strings : | ||
| List<String> ipAddresses = new ArrayList<>(); | ||
| for (InetAddress 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 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); | ||
| 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 | ||
|
|
@@ -76,7 +91,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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. report() now calls PendingHostnamesStore.clear(), introducing an unexpected side-effect and mixing responsibilities. Move this clearing to a clearly named method or document/rename report() to reflect the additional behavior. Detailsβ¨ AI Reasoning π§ How do I fix it? Reply |
||
| 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) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Map<String, Set<Integer>>> store = | ||
| ThreadLocal.withInitial(LinkedHashMap::new); | ||
|
|
||
| public static void add(String hostname, int port) { | ||
| Map<String, Set<Integer>> map = store.get(); | ||
| if (!map.containsKey(hostname)) { | ||
| map.put(hostname, new LinkedHashSet<>()); | ||
| } | ||
| map.get(hostname).add(port); | ||
| } | ||
|
|
||
| public static Set<Integer> getAndRemove(String hostname) { | ||
| Set<Integer> ports = store.get().remove(hostname); | ||
| if (ports == null) { | ||
| return Collections.emptySet(); | ||
| } | ||
| return ports; | ||
| } | ||
|
|
||
| public static Set<Integer> getPorts(String hostname) { | ||
| Set<Integer> ports = store.get().get(hostname); | ||
| if (ports == null) { | ||
| return Collections.emptySet(); | ||
| } | ||
| return Collections.unmodifiableSet(ports); | ||
| } | ||
|
|
||
| public static void clear() { | ||
| store.get().clear(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package dev.aikido.agent_api.storage.service_configuration; | ||
|
|
||
| public record Domain(String hostname, String mode) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, String> domains = new HashMap<>(); | ||
| private boolean blockNewOutgoingRequests = false; | ||
|
|
||
| public void update(List<Domain> newDomains, boolean blockNewOutgoingRequests) { | ||
| if (newDomains != null) { | ||
| this.domains = new HashMap<>(); | ||
| for (Domain domain : newDomains) { | ||
| this.domains.put(domain.hostname(), domain.mode()); | ||
| } | ||
| } | ||
| this.blockNewOutgoingRequests = blockNewOutgoingRequests; | ||
| } | ||
|
|
||
| public boolean shouldBlockOutgoingRequest(String 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 !"allow".equals(mode); | ||
| } | ||
|
|
||
| // Only block outgoing requests if the mode is "block" | ||
| return "block".equals(mode); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.