From 19738b3ac23cb2c795ea655ebaf7a118c430c4d7 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 8 Jun 2026 17:03:09 +0200 Subject: [PATCH 1/2] Trim trailing dot and normalize case for trusted IMDS hostnames DNS resolvers may return hostnames with a trailing dot (FQDN form), e.g. `metadata.google.internal.`. The previous equality check failed to match these, risking false-positive stored-SSRF blocks for GCP IMDS. Co-Authored-By: Claude Sonnet 4.6 --- .../vulnerabilities/ssrf/imds/TrustedHosts.java | 6 +++++- .../src/test/java/vulnerabilities/ssrf/ResolverTest.java | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java index f0e36337c..a32a92d5b 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java @@ -13,6 +13,10 @@ private TrustedHosts() {} /** Checks if this hostname is trusted */ public static boolean isTrustedHostname(String hostname) { - return Arrays.asList(trustedHosts).contains(hostname); + String normalized = hostname.toLowerCase(); + if (normalized.endsWith(".")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return Arrays.asList(trustedHosts).contains(normalized); } } diff --git a/agent_api/src/test/java/vulnerabilities/ssrf/ResolverTest.java b/agent_api/src/test/java/vulnerabilities/ssrf/ResolverTest.java index 363ae06b7..a9715c22b 100644 --- a/agent_api/src/test/java/vulnerabilities/ssrf/ResolverTest.java +++ b/agent_api/src/test/java/vulnerabilities/ssrf/ResolverTest.java @@ -85,4 +85,13 @@ void testResolvesToImdsIp_WithMultipleResolvedIps_OnlyTrustedHostname() { assertNull(Resolver.resolvesToImdsIp(resolvedIps, "metadata.google.internal")); } + + @Test + void testResolvesToImdsIp_TrustedHostnameWithTrailingDot() { + Set resolvedIps = new HashSet<>(); + resolvedIps.add("169.254.169.254"); // IMDS IP + + assertNull(Resolver.resolvesToImdsIp(resolvedIps, "metadata.google.internal.")); + assertNull(Resolver.resolvesToImdsIp(resolvedIps, "metadata.goog.")); + } } From d99190e03834aba8e6637351fc72b9a70647454d Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 8 Jun 2026 17:29:16 +0200 Subject: [PATCH 2/2] Add NormalizeHostname helper; use it in TrustedHosts Create helpers/net/NormalizeHostname.java (lowercase, strip trailing dot, IDN via java.net.IDN) to give SSRF code a canonical hostname form. Replace the inline normalization in TrustedHosts.isTrustedHostname with a call to this helper. Co-Authored-By: Claude Sonnet 4.6 --- .../helpers/net/NormalizeHostname.java | 25 ++++++++++++ .../ssrf/imds/TrustedHosts.java | 9 +---- .../helpers/net/NormalizeHostnameTest.java | 40 +++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 agent_api/src/main/java/dev/aikido/agent_api/helpers/net/NormalizeHostname.java create mode 100644 agent_api/src/test/java/helpers/net/NormalizeHostnameTest.java diff --git a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/NormalizeHostname.java b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/NormalizeHostname.java new file mode 100644 index 000000000..c321e2120 --- /dev/null +++ b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/NormalizeHostname.java @@ -0,0 +1,25 @@ +package dev.aikido.agent_api.helpers.net; + +import java.net.IDN; + +public final class NormalizeHostname { + private NormalizeHostname() {} + + /** + * Canonicalizes a hostname for consistent comparison: lowercases, strips a + * trailing dot (FQDN form returned by some DNS resolvers), and converts + * Punycode labels to Unicode via IDN. + */ + public static String normalize(String hostname) { + if (hostname == null || hostname.isEmpty()) { + return hostname; + } + String lower = hostname.toLowerCase(); + String noDot = lower.endsWith(".") ? lower.substring(0, lower.length() - 1) : lower; + try { + return IDN.toUnicode(noDot); + } catch (Exception e) { + return noDot; + } + } +} diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java index a32a92d5b..c8e7c45a2 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/imds/TrustedHosts.java @@ -1,8 +1,7 @@ package dev.aikido.agent_api.vulnerabilities.ssrf.imds; +import dev.aikido.agent_api.helpers.net.NormalizeHostname; import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; public final class TrustedHosts { private TrustedHosts() {} @@ -13,10 +12,6 @@ private TrustedHosts() {} /** Checks if this hostname is trusted */ public static boolean isTrustedHostname(String hostname) { - String normalized = hostname.toLowerCase(); - if (normalized.endsWith(".")) { - normalized = normalized.substring(0, normalized.length() - 1); - } - return Arrays.asList(trustedHosts).contains(normalized); + return Arrays.asList(trustedHosts).contains(NormalizeHostname.normalize(hostname)); } } diff --git a/agent_api/src/test/java/helpers/net/NormalizeHostnameTest.java b/agent_api/src/test/java/helpers/net/NormalizeHostnameTest.java new file mode 100644 index 000000000..54d73d573 --- /dev/null +++ b/agent_api/src/test/java/helpers/net/NormalizeHostnameTest.java @@ -0,0 +1,40 @@ +package helpers.net; + +import dev.aikido.agent_api.helpers.net.NormalizeHostname; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class NormalizeHostnameTest { + + @Test + void testNullAndEmpty() { + assertNull(NormalizeHostname.normalize(null)); + assertEquals("", NormalizeHostname.normalize("")); + } + + @Test + void testLowercases() { + assertEquals("example.com", NormalizeHostname.normalize("EXAMPLE.COM")); + assertEquals("metadata.google.internal", NormalizeHostname.normalize("METADATA.GOOGLE.INTERNAL")); + } + + @Test + void testStripsTrailingDot() { + assertEquals("example.com", NormalizeHostname.normalize("example.com.")); + assertEquals("metadata.google.internal", NormalizeHostname.normalize("metadata.google.internal.")); + assertEquals("metadata.goog", NormalizeHostname.normalize("metadata.goog.")); + } + + @Test + void testStripsTrailingDotAndLowercases() { + assertEquals("metadata.google.internal", NormalizeHostname.normalize("METADATA.GOOGLE.INTERNAL.")); + } + + @Test + void testPlainHostnameUnchanged() { + assertEquals("example.com", NormalizeHostname.normalize("example.com")); + assertEquals("localhost", NormalizeHostname.normalize("localhost")); + } +}