Files
gh-christianlouis-docuelevate/app/utils/network.py
T
copilot-swe-agent[bot] 8984d4da70 fix: resolve merge conflicts and refactor join_url to use urllib.parse
- Resolve merge conflicts in .jules/sentinel.md and app/utils/network.py
- Refactor join_url() to use urllib.parse.urlsplit/urlunsplit and posixpath
  instead of sentinel-string hack, preventing corruption for any input URL
- Fix test to use pytest tmp_path fixture instead of hard-coded /tmp/workdir
2026-03-23 16:17:01 +00:00

64 lines
2.5 KiB
Python

import ipaddress
import logging
import posixpath
import socket
from urllib.parse import urlsplit, urlunsplit
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.
# Fail securely: block unresolved domains to prevent DNS rebinding
# and SSRF bypasses via unresolvable addresses.
logger.warning(f"Could not resolve hostname (blocking securely): {hostname}")
return True
def join_url(base: str, *parts: str) -> str:
"""
Safely join a base URL with one or more path parts.
Uses urllib.parse to correctly handle scheme/netloc/query/fragment so that
only the path component is normalised (double slashes removed via
posixpath.join). The scheme separator ``://`` is therefore never at risk
of being collapsed.
Examples:
join_url("https://example.com/dav/", "/remote/", "file.pdf")
-> "https://example.com/dav/remote/file.pdf"
"""
parsed = urlsplit(base)
# Strip leading/trailing slashes from every part so posixpath.join
# produces a clean joined path without accidental double slashes.
stripped_parts = [p.strip("/") for p in parts if p.strip("/")]
base_path = parsed.path.rstrip("/")
if stripped_parts:
new_path = base_path + "/" + "/".join(stripped_parts)
else:
new_path = base_path
# Normalise any remaining double slashes in the path only.
new_path = posixpath.normpath(new_path) if new_path else "/"
return urlunsplit((parsed.scheme, parsed.netloc, new_path, parsed.query, parsed.fragment))