Skip to content

Commit e345677

Browse files
Merge pull request steam-bell-92#1660 from Kirtan-pc/fix/URLSanitizer
Fix: URLSanitizer allows SSRF via hostnames without DNS/IP revalidation
2 parents 5f371d0 + 3861789 commit e345677

2 files changed

Lines changed: 59 additions & 5 deletions

File tree

security/url_sanitizer.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import re
8+
import socket
89
from urllib.parse import urlparse, urlunparse
910
from typing import Optional, List, Set
1011
from ipaddress import ip_address, ip_network
@@ -110,17 +111,49 @@ def validate_url(self, url: str) -> str:
110111
return sanitized
111112

112113
def _validate_ip_address(self, host: str) -> None:
113-
"""Validate IP address against blocked networks."""
114+
"""Validate IP address against blocked networks, resolving hostnames via DNS."""
114115
try:
115116
ip = ip_address(host)
116-
except ValueError:
117+
self._check_blocked_ip(ip, host)
117118
return
118-
119+
except ValueError:
120+
pass
121+
122+
try:
123+
addrinfo = socket.getaddrinfo(host, None)
124+
except socket.gaierror:
125+
raise InvalidURLError(f"Failed to resolve hostname: {host}")
126+
127+
seen = set()
128+
for _, _, _, _, sockaddr in addrinfo:
129+
ip = ip_address(sockaddr[0])
130+
if ip in seen:
131+
continue
132+
seen.add(ip)
133+
self._check_blocked_ip(ip, host)
134+
135+
def _check_blocked_ip(self, ip, host):
136+
"""Check a single IP against blocked networks."""
119137
for network in self._blocked_networks:
120138
if ip in network:
121139
if not (self.allow_localhost and ip.is_loopback):
122140
if not (self.allow_private and ip.is_private):
123141
raise InvalidURLError(f"Blocked IP address: {host}")
142+
143+
def _validate_port(self, port: Optional[int]) -> None:
144+
"""Validate port number against allowed/blocked lists."""
145+
if port is None:
146+
return
147+
if port < 1 or port > 65535:
148+
raise InvalidURLError(f"Invalid port number: {port}")
149+
if self.allowed_ports is not None:
150+
if port not in self.allowed_ports:
151+
raise InvalidURLError(
152+
f"Port {port} is not allowed. "
153+
f"Allowed ports: {', '.join(str(p) for p in self.allowed_ports)}"
154+
)
155+
elif port in self.BLOCKED_PORTS:
156+
raise InvalidURLError(f"Blocked port: {port}")
124157

125158
def _validate_port(self, port: Optional[int]) -> None:
126159
"""Validate port number against allowed/blocked lists."""

tests/test_security.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,36 @@ 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_hostname_resolves_to_private_ip(self):
82+
"""Test that hostnames resolving to private IPs are blocked."""
83+
sanitizer = URLSanitizer(allow_localhost=False, allow_private=False)
84+
with pytest.raises(InvalidURLError):
85+
sanitizer.validate_url("http://localhost")
86+
87+
def test_hostname_resolves_to_private_ip_with_allow_localhost(self):
88+
sanitizer = URLSanitizer(allow_localhost=True)
89+
result = sanitizer.validate_url("http://localhost")
90+
assert result.startswith("http://localhost")
91+
92+
def test_hostname_resolves_to_private_ip_with_allow_private(self):
93+
sanitizer = URLSanitizer(allow_private=True)
94+
result = sanitizer.validate_url("http://localhost")
95+
assert result.startswith("http://localhost")
96+
97+
def test_unresolvable_hostname_raises_error(self):
98+
sanitizer = URLSanitizer()
99+
with pytest.raises(InvalidURLError):
100+
sanitizer.validate_url("http://this-domain-does-not-exist-12345.com")
101+
81102
def test_blocked_port(self):
82103
sanitizer = URLSanitizer()
83104
with pytest.raises(InvalidURLError):
84105
sanitizer.validate_url("http://example.com:22")
85106

86-
def test_blocked_port_on_ip_literal(self):
107+
def test_blocked_port_with_hostname_private_ip(self):
87108
sanitizer = URLSanitizer(allow_localhost=True)
88109
with pytest.raises(InvalidURLError):
89-
sanitizer.validate_url("http://127.0.0.1:22")
110+
sanitizer.validate_url("http://localhost:22")
90111

91112
def test_allowed_ports_allowlist(self):
92113
sanitizer = URLSanitizer(allowed_ports=[80, 443])

0 commit comments

Comments
 (0)