Skip to content
Open
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
17 changes: 17 additions & 0 deletions aikido_zen/helpers/get_trusted_hostnames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Helper function file, see function docstring"""

import os


def get_trusted_hostnames():
"""
Parses the AIKIDO_TRUSTED_HOSTNAMES environment variable.
Returns a list of hostnames that should be considered as the server itself
(i.e. outgoing requests to these hosts will not be flagged as SSRF).
The value is expected to be a comma-separated list of hostnames, e.g.:
AIKIDO_TRUSTED_HOSTNAMES=myapp.com,api.myapp.com
"""
env_value = os.getenv("AIKIDO_TRUSTED_HOSTNAMES")
if not env_value:
return []
return [h.strip() for h in env_value.split(",") if h.strip()]
6 changes: 3 additions & 3 deletions aikido_zen/vulnerabilities/ssrf/find_hostname_in_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ def find_hostname_in_context(hostname, context: Context, port):
# Validate hostname and port input
return None

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

# Gets the different hostname options: with/without punycode, with/without brackets for IPv6
Expand Down
41 changes: 8 additions & 33 deletions aikido_zen/vulnerabilities/ssrf/is_request_to_itself.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,14 @@
from aikido_zen.helpers.get_ip_from_request import trust_proxy
from aikido_zen.helpers.get_port_from_url import get_port_from_url
from aikido_zen.helpers.try_parse_url import try_parse_url
from aikido_zen.helpers.get_trusted_hostnames import get_trusted_hostnames


def is_request_to_itself(server_url, outbound_hostname, outbound_port):
def is_request_to_itself(outbound_hostname):
"""
We don't want to block outgoing requests to the same host as the server
(often happens that we have a match on headers like `Host`, `Origin`, `Referer`, etc.)
We have to check the port as well, because the hostname can be the same but with a different port
We don't want to block outgoing requests to hostnames that are explicitly
declared as the server itself via AIKIDO_TRUSTED_HOSTNAMES. Customers must explicitly list their own hostnames in
the AIKIDO_TRUSTED_HOSTNAMES environment variable (comma-separated).
"""

# When the app is not behind a reverse proxy, we can't trust the hostname inside `server_url`
# The hostname in `server_url` is built from the request headers
# The headers can be manipulated by the client if the app is directly exposed to the internet
if not trust_proxy():
return False

base_url = try_parse_url(server_url)
if base_url is None:
return False

if base_url.hostname != outbound_hostname:
if not outbound_hostname or not isinstance(outbound_hostname, str):
return False

base_url_port = get_port_from_url(base_url, parsed=True)

# If the port and hostname are the same, the server is making a request to itself
if base_url_port == outbound_port:
return True

# Special case for HTTP/HTTPS ports
# In production, the app will be served on port 80 and 443
if base_url_port == 80 and outbound_port == 443:
return True
if base_url_port == 443 and outbound_port == 80:
return True

return False
trusted = get_trusted_hostnames()
return outbound_hostname in trusted
67 changes: 28 additions & 39 deletions aikido_zen/vulnerabilities/ssrf/is_request_to_itself_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,47 @@

@pytest.fixture(autouse=True)
def clear_environment():
# Clear the environment variable before each test
os.environ.pop("AIKIDO_TRUST_PROXY", None)
os.environ.pop("AIKIDO_TRUSTED_HOSTNAMES", None)
yield
os.environ.pop("AIKIDO_TRUSTED_HOSTNAMES", None)


def test_returns_false_if_server_url_is_empty():
assert not is_request_to_itself("", "aikido.dev", 80)
def test_returns_false_if_no_trusted_hostnames_configured():
assert not is_request_to_itself("aikido.dev")
assert not is_request_to_itself("localhost")


def test_returns_false_if_server_url_is_invalid():
assert not is_request_to_itself("http://", "aikido.dev", 80)
def test_returns_false_if_hostname_not_in_trusted_list(monkeypatch):
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "myapp.com,api.myapp.com")
assert not is_request_to_itself("aikido.dev")
assert not is_request_to_itself("google.com")


def test_returns_false_if_port_is_different():
assert not is_request_to_itself("http://aikido.dev:4000", "aikido.dev", 80)
assert not is_request_to_itself("https://aikido.dev:4000", "aikido.dev", 443)
def test_returns_true_if_hostname_in_trusted_list(monkeypatch):
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "myapp.com,api.myapp.com")
assert is_request_to_itself("myapp.com")
assert is_request_to_itself("api.myapp.com")


def test_returns_false_if_hostname_is_different():
assert not is_request_to_itself("http://aikido.dev", "google.com", 80)
assert not is_request_to_itself("http://aikido.dev:4000", "google.com", 4000)
assert not is_request_to_itself("https://aikido.dev", "google.com", 443)
assert not is_request_to_itself("https://aikido.dev:4000", "google.com", 443)
def test_returns_true_for_single_trusted_hostname(monkeypatch):
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", "aikido.dev")
assert is_request_to_itself("aikido.dev")


def test_returns_true_if_server_does_request_to_itself():
assert is_request_to_itself("https://aikido.dev", "aikido.dev", 443)
assert is_request_to_itself("http://aikido.dev:4000", "aikido.dev", 4000)
assert is_request_to_itself("http://aikido.dev", "aikido.dev", 80)
assert is_request_to_itself("https://aikido.dev:4000", "aikido.dev", 4000)
def test_strips_whitespace_from_trusted_hostnames(monkeypatch):
monkeypatch.setenv("AIKIDO_TRUSTED_HOSTNAMES", " myapp.com , api.myapp.com ")
assert is_request_to_itself("myapp.com")
assert is_request_to_itself("api.myapp.com")


def test_returns_true_for_special_case_http_to_https():
assert is_request_to_itself("http://aikido.dev", "aikido.dev", 443)
assert is_request_to_itself("https://aikido.dev", "aikido.dev", 80)
def test_returns_false_if_hostname_is_none():
assert not is_request_to_itself(None)


def test_returns_false_if_trust_proxy_is_false(monkeypatch):
monkeypatch.setenv("AIKIDO_TRUST_PROXY", "false")
assert not is_request_to_itself("https://aikido.dev", "aikido.dev", 443)
assert not is_request_to_itself("http://aikido.dev", "aikido.dev", 80)
def test_returns_false_if_hostname_is_empty():
assert not is_request_to_itself("")


def test_returns_false_if_server_url_is_null():
assert not is_request_to_itself(None, "aikido.dev", 80)
assert not is_request_to_itself(None, "aikido.dev", 443)


def test_returns_false_if_hostname_is_null():
assert not is_request_to_itself("http://aikido.dev:4000", None, 80)
assert not is_request_to_itself("https://aikido.dev:4000", None, 443)


def test_returns_false_if_both_are_null():
assert not is_request_to_itself(None, None, 80)
assert not is_request_to_itself(None, None, 443)
def test_returns_false_if_hostname_is_not_a_string():
assert not is_request_to_itself(123)
assert not is_request_to_itself(["myapp.com"])
12 changes: 12 additions & 0 deletions docs/ssrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ In this example, the attacker sends a request to `localtest.me:3000/private`, wh

We don't protect against stored SSRF attacks, where an attacker injects a malicious URL into your app's database. To prevent stored SSRF attacks, validate and sanitize user input before storing it in your database.

## Allowlisting your own hostnames (`AIKIDO_TRUSTED_HOSTNAMES`)

To safely allow outgoing requests to your own services, set the `AIKIDO_TRUSTED_HOSTNAMES` environment variable to a comma-separated list of your hostnames:

```
AIKIDO_TRUSTED_HOSTNAMES=myapp.com,api.myapp.com,backend.internal
```
Any outgoing request whose destination hostname exactly matches one of these values will not be flagged as SSRF

**When should you set this?**
Set `AIKIDO_TRUSTED_HOSTNAMES` when your application makes outgoing requests to its own domain, and those hostnames appear in user-supplied input (e.g. `Host`, `Referer`, `Origin`, etc.)

## Which built-in modules are protected?

Firewall protects against SSRF attacks in the following built-in modules:
Expand Down
Loading