mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-27 16:22:20 +01:00
fix(security): harden Python service boundaries
This commit is contained in:
@@ -154,3 +154,52 @@ def check_outbound_url(
|
||||
if not saw_ip:
|
||||
return False, "host does not resolve to an IP"
|
||||
return True, "ok"
|
||||
|
||||
|
||||
|
||||
class OutboundAddressBlocked(PermissionError):
|
||||
"""A non-HTTP outbound host resolves into a disallowed address range."""
|
||||
|
||||
|
||||
def connect_outbound_tcp(
|
||||
host: str,
|
||||
port: int,
|
||||
*,
|
||||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
block_private: bool = False,
|
||||
source_address=None,
|
||||
resolver: Optional[Callable[..., list]] = None,
|
||||
) -> socket.socket:
|
||||
"""Open a TCP connection to *host* under the outbound address policy.
|
||||
|
||||
For raw TCP clients (IMAP/SMTP). The host is resolved exactly once, every
|
||||
resolved address is judged with the same policy as check_outbound_url, and
|
||||
the socket connects only to those already-judged addresses. A second
|
||||
lookup at connect time (what socket.create_connection(host) does) would let
|
||||
a rebinding name pass the check with a public answer and then connect to
|
||||
metadata/private space. TLS callers keep wrapping the returned socket with
|
||||
``server_hostname=host``, so SNI and certificate checks are unchanged.
|
||||
"""
|
||||
resolve = resolver or socket.getaddrinfo
|
||||
infos = resolve(host, port, 0, socket.SOCK_STREAM)
|
||||
for _family, _type, _proto, _canon, sockaddr in infos:
|
||||
ip = ipaddress.ip_address(str(sockaddr[0]).split("%")[0])
|
||||
reason = _classify(ip, block_private=block_private)
|
||||
if reason:
|
||||
raise OutboundAddressBlocked(reason)
|
||||
last_error: Optional[OSError] = None
|
||||
for family, socktype, proto, _canon, sockaddr in infos:
|
||||
sock = socket.socket(family, socktype, proto)
|
||||
try:
|
||||
if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: # same contract as create_connection
|
||||
sock.settimeout(timeout)
|
||||
if source_address:
|
||||
sock.bind(source_address)
|
||||
sock.connect(sockaddr)
|
||||
return sock
|
||||
except OSError as exc:
|
||||
last_error = exc
|
||||
sock.close()
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
raise socket.gaierror(socket.EAI_NONAME, f"{host} did not resolve to an address")
|
||||
|
||||
Reference in New Issue
Block a user