From ed966696eb335a91766d2819c15883dde02ef317 Mon Sep 17 00:00:00 2001 From: sprmn24 Date: Mon, 25 May 2026 18:23:44 +0300 Subject: [PATCH] fix(security): handle IPv6 scope IDs in URL safety checks to prevent bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ipaddress.ip_address() raises ValueError on IPv6 addresses with scope IDs (e.g. 'fe80::1%eth0'). Both is_always_blocked_url() and is_safe_url() silently skipped these via `except ValueError: continue`. If ALL resolved addresses for a hostname carry scope IDs, every address is skipped and the URL passes all safety checks — a potential SSRF bypass vector against link-local or metadata endpoints. Fix: - Strip the scope ID (%eth0) before parsing in both functions - is_safe_url(): fail closed (return False) with a warning log if still unparseable after stripping - is_always_blocked_url(): use continue (not return False) to preserve multi-address scanning, with a warning log Affected: tools/url_safety.py — is_always_blocked_url(), is_safe_url() --- tools/url_safety.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/url_safety.py b/tools/url_safety.py index ac6326e306f..32b0d3bddfc 100644 --- a/tools/url_safety.py +++ b/tools/url_safety.py @@ -282,9 +282,12 @@ def is_always_blocked_url(url: str) -> bool: for _family, _, _, _, sockaddr in addr_info: ip_str = sockaddr[0] + if '%' in ip_str: + ip_str = ip_str.split('%')[0] try: resolved = ipaddress.ip_address(ip_str) except ValueError: + logger.warning("Unparseable IP address %r for hostname %s — skipping address", sockaddr[0], hostname) continue if resolved in _ALWAYS_BLOCKED_IPS or any( resolved in net for net in _ALWAYS_BLOCKED_NETWORKS @@ -353,10 +356,14 @@ def is_safe_url(url: str) -> bool: for family, _, _, _, sockaddr in addr_info: ip_str = sockaddr[0] + if '%' in ip_str: + ip_str = ip_str.split('%')[0] try: ip = ipaddress.ip_address(ip_str) except ValueError: - continue + # Still unparseable after scope ID strip — fail closed + logger.warning("Blocked request — unparseable IP address %r for hostname %s", sockaddr[0], hostname) + return False # Always block cloud metadata IPs and link-local, even with toggle on if ip in _ALWAYS_BLOCKED_IPS or any(ip in net for net in _ALWAYS_BLOCKED_NETWORKS):