Skip to content

Commit 8755f70

Browse files
fix: validate URL host against allowed hosts whitelist (#573)
The is_url_host_valid method in AllowedHostsValidator was only checking that a URL had a valid scheme and netloc, but never actually compared the host against the configured allowed_hosts set. This meant any URL with a valid structure would pass validation regardless of host. Now the method correctly extracts the hostname from the URL and checks it against the allowed_hosts set (case-insensitive). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a0e7a88 commit 8755f70

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

packages/abstractions/kiota_abstractions/authentication/allowed_hosts_validator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,8 @@ def is_url_host_valid(self, url: str) -> bool:
6363
# Returns: ParseResult(scheme='scheme', netloc='netloc', path='/path;parameters', params='',
6464
# query='query', fragment='fragment')
6565
o = urlparse(url)
66-
return all([o.scheme, o.netloc])
66+
if not all([o.scheme, o.netloc]):
67+
return False
68+
if not o.hostname:
69+
return False
70+
return o.hostname.lower() in self.allowed_hosts
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pytest
2+
3+
from kiota_abstractions.authentication.allowed_hosts_validator import AllowedHostsValidator
4+
5+
6+
class TestAllowedHostsValidatorInit:
7+
def test_creates_with_valid_hosts(self):
8+
validator = AllowedHostsValidator(["example.com", "api.example.com"])
9+
assert set(validator.get_allowed_hosts()) == {"example.com", "api.example.com"}
10+
11+
def test_lowercases_hosts(self):
12+
validator = AllowedHostsValidator(["Example.COM", "API.Example.com"])
13+
assert set(validator.get_allowed_hosts()) == {"example.com", "api.example.com"}
14+
15+
def test_raises_on_non_list(self):
16+
with pytest.raises(TypeError):
17+
AllowedHostsValidator("example.com")
18+
19+
def test_raises_on_http_prefix(self):
20+
with pytest.raises(ValueError):
21+
AllowedHostsValidator(["http://example.com"])
22+
23+
def test_raises_on_https_prefix(self):
24+
with pytest.raises(ValueError):
25+
AllowedHostsValidator(["https://example.com"])
26+
27+
28+
class TestIsUrlHostValid:
29+
def test_returns_false_for_empty_url(self):
30+
validator = AllowedHostsValidator(["example.com"])
31+
assert validator.is_url_host_valid("") is False
32+
33+
def test_returns_false_for_none_url(self):
34+
validator = AllowedHostsValidator(["example.com"])
35+
assert validator.is_url_host_valid(None) is False
36+
37+
def test_returns_true_when_allowed_hosts_empty(self):
38+
validator = AllowedHostsValidator([])
39+
assert validator.is_url_host_valid("https://anything.com/path") is True
40+
41+
def test_returns_true_for_allowed_host(self):
42+
validator = AllowedHostsValidator(["example.com"])
43+
assert validator.is_url_host_valid("https://example.com/path") is True
44+
45+
def test_returns_false_for_disallowed_host(self):
46+
validator = AllowedHostsValidator(["example.com"])
47+
assert validator.is_url_host_valid("https://evil.com/path") is False
48+
49+
def test_host_matching_is_case_insensitive(self):
50+
validator = AllowedHostsValidator(["example.com"])
51+
assert validator.is_url_host_valid("https://EXAMPLE.COM/path") is True
52+
53+
def test_returns_false_for_url_without_scheme(self):
54+
validator = AllowedHostsValidator(["example.com"])
55+
assert validator.is_url_host_valid("example.com/path") is False
56+
57+
def test_returns_false_for_url_without_netloc(self):
58+
validator = AllowedHostsValidator(["example.com"])
59+
assert validator.is_url_host_valid("https://") is False
60+
61+
def test_validates_subdomain_separately(self):
62+
validator = AllowedHostsValidator(["example.com"])
63+
assert validator.is_url_host_valid("https://sub.example.com/path") is False
64+
65+
def test_allows_multiple_valid_hosts(self):
66+
validator = AllowedHostsValidator(["example.com", "api.example.com"])
67+
assert validator.is_url_host_valid("https://example.com/path") is True
68+
assert validator.is_url_host_valid("https://api.example.com/path") is True
69+
assert validator.is_url_host_valid("https://other.com/path") is False
70+
71+
def test_handles_url_with_port(self):
72+
validator = AllowedHostsValidator(["example.com"])
73+
assert validator.is_url_host_valid("https://example.com:8080/path") is True
74+
75+
def test_handles_url_with_query_and_fragment(self):
76+
validator = AllowedHostsValidator(["example.com"])
77+
assert validator.is_url_host_valid("https://example.com/path?q=1#frag") is True
78+
79+
80+
class TestSetAllowedHosts:
81+
def test_updates_allowed_hosts(self):
82+
validator = AllowedHostsValidator(["old.com"])
83+
validator.set_allowed_hosts(["new.com"])
84+
assert validator.is_url_host_valid("https://new.com/path") is True
85+
assert validator.is_url_host_valid("https://old.com/path") is False
86+
87+
def test_raises_on_non_list(self):
88+
validator = AllowedHostsValidator(["example.com"])
89+
with pytest.raises(TypeError):
90+
validator.set_allowed_hosts("example.com")
91+
92+
def test_raises_on_http_prefix(self):
93+
validator = AllowedHostsValidator(["example.com"])
94+
with pytest.raises(ValueError):
95+
validator.set_allowed_hosts(["http://example.com"])

0 commit comments

Comments
 (0)