80de3b6743
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
import ipaddress
|
|
import logging
|
|
import socket
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def is_private_ip(hostname: str) -> bool:
|
|
"""
|
|
Check if a hostname resolves to a private/internal IP address.
|
|
Protects against SSRF attacks by blocking access to internal networks.
|
|
"""
|
|
try:
|
|
# Try to parse as IP address directly
|
|
ip = ipaddress.ip_address(hostname)
|
|
return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
|
|
except ValueError:
|
|
# Not a direct IP, try to resolve hostname
|
|
try:
|
|
# Get all IP addresses for this hostname
|
|
addr_info = socket.getaddrinfo(hostname, None)
|
|
for info in addr_info:
|
|
ip_str = info[4][0]
|
|
ip = ipaddress.ip_address(ip_str)
|
|
# Block if ANY resolved IP is private/internal
|
|
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
|
|
return True
|
|
return False
|
|
except (socket.gaierror, socket.error):
|
|
# Cannot resolve - allow for testing/development
|
|
# In production, DNS should work properly
|
|
# Log this for debugging
|
|
logger.warning(f"Could not resolve hostname: {hostname}")
|
|
return False # Changed from True to False to allow external domains in tests
|
|
|
|
|
|
def join_url(base: str, *parts: str) -> str:
|
|
"""
|
|
Safely join a base URL and multiple path parts.
|
|
Handles double slashes while preserving the protocol '://'.
|
|
"""
|
|
url = "/".join([base, *parts])
|
|
url = url.replace("://", "$PLACEHOLDER$")
|
|
while "//" in url:
|
|
url = url.replace("//", "/")
|
|
url = url.replace("$PLACEHOLDER$", "://")
|
|
return url
|