Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ public static void report(String hostname, InetAddress[] inetAddresses) {
}
} else if (!IPValidator.isIP(hostname)) {
// Skip literal IPs with no pending port: usually inbound IP parsing rather than an outbound request,
// and we can't tell them apart here. A real outbound one still hits the blocking/SSRF checks below.
// and we can't tell them apart here. A real outbound one still hits the SSRF checks below.
HostnamesStore.incrementHits(hostname, 0);
}

// Checked here, not at the HTTP client, since not all clients are instrumented and port/context isn't always available.
if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) {
if (!IPValidator.isIP(hostname) && ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) {
logger.debug("Blocking DNS lookup for domain: %s", hostname);
throw BlockedOutboundException.get();
}
Comment on lines 45 to 55

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low - Bracketed IPv6 URLs remain blocked in outbound lockdown

An application makes an HTTP(S) request to a literal IPv6 URL such as http://[::1]:8080 while new outbound connections are blocked. URLCollector and the DNS advice preserve the bracketed host, but IPValidator.isIP does not accept brackets, so DNSRecordCollector still applies the unknown-host outbound rule and throws BlockedOutboundException. This contradicts the PR's literal-IPv6 exemption and can prevent legitimate direct IPv6 connections from being made.

Show fix
Suggested change
} else if (!IPValidator.isIP(hostname)) {
// Skip literal IPs with no pending port: usually inbound IP parsing rather than an outbound request,
// and we can't tell them apart here. A real outbound one still hits the blocking/SSRF checks below.
// and we can't tell them apart here. A real outbound one still hits the SSRF checks below.
HostnamesStore.incrementHits(hostname, 0);
}
// Checked here, not at the HTTP client, since not all clients are instrumented and port/context isn't always available.
if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) {
if (!IPValidator.isIP(hostname) && ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) {
logger.debug("Blocking DNS lookup for domain: %s", hostname);
throw BlockedOutboundException.get();
}
} else if (!IPValidator.isIP(hostname)
&& !(hostname.startsWith("[")
&& hostname.endsWith("]")
&& IPValidator.isIP(hostname.substring(1, hostname.length() - 1)))) {
// Skip literal IPs with no pending port: usually inbound IP parsing rather than an outbound request,
// and we can't tell them apart here. A real outbound one still hits the SSRF checks below.
HostnamesStore.incrementHits(hostname, 0);
}
// Checked here, not at the HTTP client, since not all clients are instrumented and port/context isn't always available.
if (!IPValidator.isIP(hostname)
&& !(hostname.startsWith("[")
&& hostname.endsWith("]")
&& IPValidator.isIP(hostname.substring(1, hostname.length() - 1)))
&& ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) {
logger.debug("Blocking DNS lookup for domain: %s", hostname);
throw BlockedOutboundException.get();
}

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

Expand Down
12 changes: 6 additions & 6 deletions agent_api/src/test/java/collectors/DNSRecordCollectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,26 +211,26 @@ public void testPrivateIpLiteralWithNoPendingPortNotRecorded() {
}

@Test
public void testPrivateIpLiteralWithNoPendingPortNotRecordedButBlockedInLockdown() {
public void testPrivateIpLiteralWithNoPendingPortNotRecordedOrBlockedInLockdown() {
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
true, null, 0L, null, null, null, true, List.of(), true, true, List.of(), 0
));
Context.set(null);
assertThrows(BlockedOutboundException.class, () ->
assertDoesNotThrow(() ->
DNSRecordCollector.report("10.20.11.143", new InetAddress[]{inetAddress2})
);
assertEquals(0, HostnamesStore.getHostnamesAsList().length);
}

@Test
public void testPrivateIpLiteralWithPendingPortStillRecordedAndBlockedInLockdown() {
public void testPrivateIpLiteralWithPendingPortStillRecordedButNotBlockedInLockdown() {
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
true, null, 0L, null, null, null, true, List.of(), true, true, List.of(), 0
));
PendingHostnamesStore.add("10.20.11.143", 443);
Context.set(mock(ContextObject.class));

assertThrows(BlockedOutboundException.class, () ->
assertDoesNotThrow(() ->
DNSRecordCollector.report("10.20.11.143", new InetAddress[]{inetAddress2})
);
Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList();
Expand Down Expand Up @@ -284,12 +284,12 @@ public void testPublicIpLiteralWithPendingPortStillRecorded() {
}

@Test
public void testPublicIpLiteralWithNoPendingPortNotRecordedButBlockedInLockdown() {
public void testPublicIpLiteralWithNoPendingPortNotRecordedOrBlockedInLockdown() {
ServiceConfigStore.updateFromAPIResponse(new APIResponse(
true, null, 0L, null, null, null, true, List.of(), true, true, List.of(), 0
));
Context.set(null);
assertThrows(BlockedOutboundException.class, () ->
assertDoesNotThrow(() ->
DNSRecordCollector.report("8.8.8.8", new InetAddress[]{inetAddress1})
);
assertEquals(0, HostnamesStore.getHostnamesAsList().length);
Expand Down
9 changes: 9 additions & 0 deletions docs/outbound-domain-caveat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Outbound domain blocking caveat

Java's `InetAddress.getByName()` handles both DNS lookups and parsing IP addresses. For example, Tomcat calls it to check whether an incoming request came through a trusted load balancer or proxy, without opening an outbound connection.

To avoid blocking these checks, Zen for Java skips literal IPv4 and IPv6 addresses when enforcing outbound domain rules. It also omits them from outbound reporting unless it can link them to an HTTP request.

Requests made directly to an IP bypass "block new outbound connections" and explicit IP block entries. Named domains still follow your allow/block rules, and SSRF checks still run.

Other Zen agents report outbound HTTP(S) traffic to literal IPs and block unapproved IP destinations when "block new outbound connections" is enabled. We're working on closing this gap in Java without disrupting inbound IP parsing.
Loading