Skip to content

Commit 93c0678

Browse files
Merge pull request #199 from AikidoSec/fix-issue-with-hostnames
SSRF: do not flag attacks using service hostnames
2 parents 76504ef + 5b0b3de commit 93c0678

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
lines changed

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/ssrf/FindHostnameInContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import static dev.aikido.agent_api.helpers.url.UrlParser.tryParseUrl;
1212
import static dev.aikido.agent_api.vulnerabilities.ssrf.RequestToItselfChecker.isRequestToItself;
13+
import static dev.aikido.agent_api.vulnerabilities.ssrf.RequestToServiceHostnameChecker.isRequestToServiceHostname;
1314

1415
public final class FindHostnameInContext {
1516
private FindHostnameInContext() {}
@@ -22,6 +23,11 @@ public static Res findHostnameInContext(String hostname, ContextObject context,
2223
return null;
2324
}
2425

26+
// We don't want to block outgoing requests where the hostname is a service name, even if it's inside user input
27+
if (isRequestToServiceHostname(hostname)) {
28+
return null;
29+
}
30+
2531
Map<String, Map<String, String>> stringsFromContext = new StringsFromContext(context).getAll();
2632
for (Map.Entry<String, Map<String, String>> sourceEntry : stringsFromContext.entrySet()) {
2733
String source = sourceEntry.getKey();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.aikido.agent_api.vulnerabilities.ssrf;
2+
3+
import java.util.List;
4+
import java.util.regex.Pattern;
5+
6+
public final class RequestToServiceHostnameChecker {
7+
// Pattern allows alphanumerical input (case-insensitive), dashes (-) and underscores (_)
8+
private static final Pattern SERVICE_HOSTNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-_]+$");
9+
private static final List ALLOWED_LOCALHOST_VARIANTS = List.of(
10+
"localhost", "localdomain"
11+
);
12+
13+
public static boolean isRequestToServiceHostname(String hostname) {
14+
if (hostname == null) {
15+
return false;
16+
}
17+
if (ALLOWED_LOCALHOST_VARIANTS.contains(hostname.toLowerCase())) {
18+
// "localhost" or its variants are not service hostnames
19+
return false;
20+
}
21+
22+
return SERVICE_HOSTNAME_PATTERN.matcher(hostname).matches();
23+
}
24+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package vulnerabilities.ssrf;
2+
3+
import dev.aikido.agent_api.vulnerabilities.ssrf.RequestToServiceHostnameChecker;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
class RequestToServiceHostnameCheckerTest {
11+
12+
@Test
13+
void testValidHostnames() {
14+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid_hostname"));
15+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid-hostname"));
16+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid123"));
17+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("hostname_with_underscores-and-dashes"));
18+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("123456"));
19+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("a-b_c"));
20+
}
21+
22+
@Test
23+
void testInvalidHostnames() {
24+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(null));
25+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(""));
26+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(" "));
27+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid@hostname"));
28+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid#hostname"));
29+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid/hostname"));
30+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid:hostname"));
31+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid;hostname"));
32+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid.hostname"));
33+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid_hostname!"));
34+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("invalid-hostname*"));
35+
}
36+
37+
@Test
38+
void testEdgeCases() {
39+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("-leadingdash"));
40+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("_leadingunderscore"));
41+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("trailingdash-"));
42+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("trailingunderscore_"));
43+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("dash--dash"));
44+
assertTrue(RequestToServiceHostnameChecker.isRequestToServiceHostname("underscore__underscore"));
45+
46+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("-leadingdash."));
47+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("_leadingunderscore."));
48+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(".trailingdash-"));
49+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(".trailingunderscore_"));
50+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("dash--dash."));
51+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname(".underscore__underscore"));
52+
}
53+
54+
@Test
55+
void testMixedValidAndInvalidCharacters() {
56+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid_hostname!@#"));
57+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid-hostname$%^"));
58+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("valid123&*()"));
59+
}
60+
61+
@Test
62+
void testAllowedLocalhostVariants() {
63+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("localhost"));
64+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("localhost.localdomain"));
65+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("LOCALHOST"));
66+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("LocalHost"));
67+
68+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("Host.docker.Internal"));
69+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("host.docker.internal"));
70+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("kubernetes.docker.internal"));
71+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("KUBERNETES.DOCKER.INTERNAL"));
72+
73+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("localdomain"));
74+
}
75+
76+
@Test
77+
void testOtherLocalhostVariants() {
78+
// If you want to test other variants that are not in the allowed list
79+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("127.0.0.1"));
80+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("::1"));
81+
}
82+
83+
@Test
84+
void testAllowedIPv4Addresses() {
85+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("192.168.1.1"));
86+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("255.255.255.255"));
87+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("0.0.0.0"));
88+
}
89+
90+
@Test
91+
void testAllowedIPv6Addresses() {
92+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
93+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("::1"));
94+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("::ffff:192.168.1.1"));
95+
}
96+
97+
@Test
98+
void testAllowedNormalHostnames() {
99+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("google.com"));
100+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("subdomain.example.com"));
101+
assertFalse(RequestToServiceHostnameChecker.isRequestToServiceHostname("example.com"));
102+
}
103+
}

agent_api/src/test/java/vulnerabilities/ssrf/SSRFDetectorTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,21 @@ public void testSsrfDetectorWithRedirectToLocalhostButIsRequestToItself() throws
127127

128128
assertNull(attackData);
129129
}
130+
131+
@Test
132+
@SetEnvironmentVariable(key = "AIKIDO_TOKEN", value = "invalid-token")
133+
public void testSsrfDetectorWithServiceHostnameInRedirect() throws MalformedURLException {
134+
// Setup context :
135+
setContextAndLifecycle("http://mysql-database/ssrf-test");
136+
137+
URLCollector.report(new URL("http://mysql-database/ssrf-test"));
138+
RedirectCollector.report(new URL("http://mysql-database/ssrf-test"), new URL("http://127.0.0.1:8080"));
139+
Attack attackData = new SSRFDetector().run(
140+
"127.0.0.1", 8080,
141+
List.of("127.0.0.1"),
142+
"testop"
143+
);
144+
145+
assertNull(attackData);
146+
}
130147
}

0 commit comments

Comments
 (0)