Skip to content

Commit 5f371d0

Browse files
Merge pull request steam-bell-92#1661 from Kirtan-pc/fix/no-validation
Fix: URLSanitizer does not validate/limit URL ports and can permit access to sensitive ports
2 parents 8622ee3 + 87da12d commit 5f371d0

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

security/url_sanitizer.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,32 @@ class URLSanitizer:
2929
"file", "gopher", "data", "javascript", "vbscript",
3030
"about", "view-source", "ws", "wss"
3131
}
32-
32+
33+
BLOCKED_PORTS: Set[int] = {
34+
7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42,
35+
43, 53, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110,
36+
111, 113, 115, 117, 119, 123, 135, 139, 143, 179, 389,
37+
443, 445, 465, 512, 513, 514, 515, 526, 530, 531, 532,
38+
540, 548, 554, 556, 563, 587, 601, 636, 993, 995,
39+
1433, 1434, 1521, 2049, 2375, 2376, 3128, 3306, 3389,
40+
4333, 5432, 5900, 5901, 6000, 6001, 6379, 6666, 6667,
41+
6668, 6669, 7001, 7002, 8000, 8080, 8081, 8443, 9000,
42+
9001, 9090, 9200, 9300, 11211, 27017, 27018, 27019,
43+
}
44+
3345
def __init__(
3446
self,
3547
allowed_schemes: Optional[List[str]] = None,
3648
allow_localhost: bool = False,
3749
allow_private: bool = False,
38-
max_length: int = 2048
50+
max_length: int = 2048,
51+
allowed_ports: Optional[List[int]] = None,
3952
):
4053
self.allowed_schemes = allowed_schemes or ['http', 'https']
4154
self.allow_localhost = allow_localhost
4255
self.allow_private = allow_private
4356
self.max_length = max_length
57+
self.allowed_ports = allowed_ports
4458
self._blocked_networks = [ip_network(net) for net in self.BLOCKED_NETWORKS]
4559

4660
def validate_url(self, url: str) -> str:
@@ -79,7 +93,8 @@ def validate_url(self, url: str) -> str:
7993
host = parsed.hostname
8094
if not host:
8195
raise InvalidURLError("URL must have a valid hostname")
82-
96+
97+
self._validate_port(parsed.port)
8398
self._validate_ip_address(host)
8499
self._validate_characters(url)
85100

@@ -107,6 +122,21 @@ def _validate_ip_address(self, host: str) -> None:
107122
if not (self.allow_private and ip.is_private):
108123
raise InvalidURLError(f"Blocked IP address: {host}")
109124

125+
def _validate_port(self, port: Optional[int]) -> None:
126+
"""Validate port number against allowed/blocked lists."""
127+
if port is None:
128+
return
129+
if port < 1 or port > 65535:
130+
raise InvalidURLError(f"Invalid port number: {port}")
131+
if self.allowed_ports is not None:
132+
if port not in self.allowed_ports:
133+
raise InvalidURLError(
134+
f"Port {port} is not allowed. "
135+
f"Allowed ports: {', '.join(str(p) for p in self.allowed_ports)}"
136+
)
137+
elif port in self.BLOCKED_PORTS:
138+
raise InvalidURLError(f"Blocked port: {port}")
139+
110140
def _validate_characters(self, url: str) -> None:
111141
"""Check for unsafe or malicious characters."""
112142
if '\0' in url:
@@ -134,8 +164,13 @@ def is_safe_url(self, url: str) -> bool:
134164
def validate_url(
135165
url: str,
136166
allowed_schemes: Optional[List[str]] = None,
167+
allowed_ports: Optional[List[int]] = None,
137168
**kwargs
138169
) -> str:
139170
"""Convenience function to validate a URL."""
140-
sanitizer = URLSanitizer(allowed_schemes=allowed_schemes, **kwargs)
171+
sanitizer = URLSanitizer(
172+
allowed_schemes=allowed_schemes,
173+
allowed_ports=allowed_ports,
174+
**kwargs
175+
)
141176
return sanitizer.validate_url(url)

tests/test_security.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,31 @@ def test_allow_private_ips(self):
7878
result = sanitizer.validate_url(url)
7979
assert result.startswith("http://10.0.0.1")
8080

81+
def test_blocked_port(self):
82+
sanitizer = URLSanitizer()
83+
with pytest.raises(InvalidURLError):
84+
sanitizer.validate_url("http://example.com:22")
85+
86+
def test_blocked_port_on_ip_literal(self):
87+
sanitizer = URLSanitizer(allow_localhost=True)
88+
with pytest.raises(InvalidURLError):
89+
sanitizer.validate_url("http://127.0.0.1:22")
90+
91+
def test_allowed_ports_allowlist(self):
92+
sanitizer = URLSanitizer(allowed_ports=[80, 443])
93+
result = sanitizer.validate_url("http://example.com:80")
94+
assert result.startswith("http://example.com:80")
95+
96+
def test_allowed_ports_rejects_non_allowed(self):
97+
sanitizer = URLSanitizer(allowed_ports=[80, 443])
98+
with pytest.raises(InvalidURLError):
99+
sanitizer.validate_url("http://example.com:8080")
100+
101+
def test_valid_port_not_blocked(self):
102+
sanitizer = URLSanitizer()
103+
result = sanitizer.validate_url("http://example.com:8888")
104+
assert result.startswith("http://example.com:8888")
105+
81106
def test_blocked_schemes(self):
82107
# These should be blocked because they're in BLOCKED_SCHEMES
83108
sanitizer = URLSanitizer()

0 commit comments

Comments
 (0)