Skip to content

Commit 19674ee

Browse files
committed
is_request_to_itself: check AIKIDO_TRUSTED_HOSTNAMES
1 parent 7d2d777 commit 19674ee

3 files changed

Lines changed: 39 additions & 75 deletions

File tree

aikido_zen/vulnerabilities/ssrf/find_hostname_in_context.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ def find_hostname_in_context(hostname, context: Context, port):
1515
# Validate hostname and port input
1616
return None
1717

18-
# We don't want to block outgoing requests to the same host as the server
19-
# (often happens that we have a match on headers like `Host`, `Origin`, `Referer`, etc.)
20-
if is_request_to_itself(context.url, hostname, port):
18+
# We don't want to block outgoing requests to hostnames the operator has
19+
# declared as their own server via AIKIDO_TRUSTED_HOSTNAMES.
20+
if is_request_to_itself(hostname):
2121
return None
2222

2323
# Gets the different hostname options: with/without punycode, with/without brackets for IPv6
Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,14 @@
1-
from aikido_zen.helpers.get_ip_from_request import trust_proxy
2-
from aikido_zen.helpers.get_port_from_url import get_port_from_url
3-
from aikido_zen.helpers.try_parse_url import try_parse_url
1+
from aikido_zen.helpers.get_trusted_hostnames import get_trusted_hostnames
42

53

6-
def is_request_to_itself(server_url, outbound_hostname, outbound_port):
4+
def is_request_to_itself(outbound_hostname):
75
"""
8-
We don't want to block outgoing requests to the same host as the server
9-
(often happens that we have a match on headers like `Host`, `Origin`, `Referer`, etc.)
10-
We have to check the port as well, because the hostname can be the same but with a different port
6+
We don't want to block outgoing requests to hostnames that are explicitly
7+
declared as the server itself via AIKIDO_TRUSTED_HOSTNAMES. Customers must explicitly list their own hostnames in
8+
the AIKIDO_TRUSTED_HOSTNAMES environment variable (comma-separated).
119
"""
12-
13-
# When the app is not behind a reverse proxy, we can't trust the hostname inside `server_url`
14-
# The hostname in `server_url` is built from the request headers
15-
# The headers can be manipulated by the client if the app is directly exposed to the internet
16-
if not trust_proxy():
17-
return False
18-
19-
base_url = try_parse_url(server_url)
20-
if base_url is None:
21-
return False
22-
23-
if base_url.hostname != outbound_hostname:
10+
if not outbound_hostname or not isinstance(outbound_hostname, str):
2411
return False
2512

26-
base_url_port = get_port_from_url(base_url, parsed=True)
27-
28-
# If the port and hostname are the same, the server is making a request to itself
29-
if base_url_port == outbound_port:
30-
return True
31-
32-
# Special case for HTTP/HTTPS ports
33-
# In production, the app will be served on port 80 and 443
34-
if base_url_port == 80 and outbound_port == 443:
35-
return True
36-
if base_url_port == 443 and outbound_port == 80:
37-
return True
38-
39-
return False
13+
trusted = get_trusted_hostnames()
14+
return outbound_hostname in trusted

aikido_zen/vulnerabilities/ssrf/is_request_to_itself_test.py

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,47 @@
66

77
@pytest.fixture(autouse=True)
88
def clear_environment():
9-
# Clear the environment variable before each test
10-
os.environ.pop("AIKIDO_TRUST_PROXY", None)
9+
os.environ.pop("AIKIDO_TRUSTED_HOSTNAMES", None)
10+
yield
11+
os.environ.pop("AIKIDO_TRUSTED_HOSTNAMES", None)
1112

1213

13-
def test_returns_false_if_server_url_is_empty():
14-
assert not is_request_to_itself("", "aikido.dev", 80)
14+
def test_returns_false_if_no_trusted_hostnames_configured():
15+
assert not is_request_to_itself("aikido.dev")
16+
assert not is_request_to_itself("localhost")
1517

1618

17-
def test_returns_false_if_server_url_is_invalid():
18-
assert not is_request_to_itself("http://", "aikido.dev", 80)
19+
def test_returns_false_if_hostname_not_in_trusted_list(monkeypatch):
20+
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "myapp.com,api.myapp.com")
21+
assert not is_request_to_itself("aikido.dev")
22+
assert not is_request_to_itself("google.com")
1923

2024

21-
def test_returns_false_if_port_is_different():
22-
assert not is_request_to_itself("http://aikido.dev:4000", "aikido.dev", 80)
23-
assert not is_request_to_itself("https://aikido.dev:4000", "aikido.dev", 443)
25+
def test_returns_true_if_hostname_in_trusted_list(monkeypatch):
26+
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "myapp.com,api.myapp.com")
27+
assert is_request_to_itself("myapp.com")
28+
assert is_request_to_itself("api.myapp.com")
2429

2530

26-
def test_returns_false_if_hostname_is_different():
27-
assert not is_request_to_itself("http://aikido.dev", "google.com", 80)
28-
assert not is_request_to_itself("http://aikido.dev:4000", "google.com", 4000)
29-
assert not is_request_to_itself("https://aikido.dev", "google.com", 443)
30-
assert not is_request_to_itself("https://aikido.dev:4000", "google.com", 443)
31+
def test_returns_true_for_single_trusted_hostname(monkeypatch):
32+
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "aikido.dev")
33+
assert is_request_to_itself("aikido.dev")
3134

3235

33-
def test_returns_true_if_server_does_request_to_itself():
34-
assert is_request_to_itself("https://aikido.dev", "aikido.dev", 443)
35-
assert is_request_to_itself("http://aikido.dev:4000", "aikido.dev", 4000)
36-
assert is_request_to_itself("http://aikido.dev", "aikido.dev", 80)
37-
assert is_request_to_itself("https://aikido.dev:4000", "aikido.dev", 4000)
36+
def test_strips_whitespace_from_trusted_hostnames(monkeypatch):
37+
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", " myapp.com , api.myapp.com ")
38+
assert is_request_to_itself("myapp.com")
39+
assert is_request_to_itself("api.myapp.com")
3840

3941

40-
def test_returns_true_for_special_case_http_to_https():
41-
assert is_request_to_itself("http://aikido.dev", "aikido.dev", 443)
42-
assert is_request_to_itself("https://aikido.dev", "aikido.dev", 80)
42+
def test_returns_false_if_hostname_is_none():
43+
assert not is_request_to_itself(None)
4344

4445

45-
def test_returns_false_if_trust_proxy_is_false(monkeypatch):
46-
monkeypatch.setenv("AIKIDO_TRUST_PROXY", "false")
47-
assert not is_request_to_itself("https://aikido.dev", "aikido.dev", 443)
48-
assert not is_request_to_itself("http://aikido.dev", "aikido.dev", 80)
46+
def test_returns_false_if_hostname_is_empty():
47+
assert not is_request_to_itself("")
4948

5049

51-
def test_returns_false_if_server_url_is_null():
52-
assert not is_request_to_itself(None, "aikido.dev", 80)
53-
assert not is_request_to_itself(None, "aikido.dev", 443)
54-
55-
56-
def test_returns_false_if_hostname_is_null():
57-
assert not is_request_to_itself("http://aikido.dev:4000", None, 80)
58-
assert not is_request_to_itself("https://aikido.dev:4000", None, 443)
59-
60-
61-
def test_returns_false_if_both_are_null():
62-
assert not is_request_to_itself(None, None, 80)
63-
assert not is_request_to_itself(None, None, 443)
50+
def test_returns_false_if_hostname_is_not_a_string():
51+
assert not is_request_to_itself(123)
52+
assert not is_request_to_itself(["myapp.com"])

0 commit comments

Comments
 (0)