diff --git a/src/url_safety.py b/src/url_safety.py index cc681703a..f85bff000 100644 --- a/src/url_safety.py +++ b/src/url_safety.py @@ -79,12 +79,18 @@ def check_outbound_url( if not raw_ips: return False, "host does not resolve" + saw_ip = False for raw in raw_ips: + if not isinstance(raw, str): + continue try: ip = ipaddress.ip_address(raw.split("%")[0]) # strip IPv6 zone id except ValueError: continue + saw_ip = True reason = _classify(ip, block_private=block_private) if reason: return False, reason + if not saw_ip: + return False, "host does not resolve to an IP" return True, "ok" diff --git a/tests/test_url_safety.py b/tests/test_url_safety.py index 8d4a18901..faae6b86b 100644 --- a/tests/test_url_safety.py +++ b/tests/test_url_safety.py @@ -68,3 +68,23 @@ def test_unresolvable_host_blocked(): ok, reason = check_outbound_url("http://does-not-resolve.invalid", resolver=PUBLIC) assert ok is False assert "resolve" in reason + + +def test_resolver_values_must_include_a_parseable_ip(): + ok, reason = check_outbound_url( + "https://example.test", + resolver=lambda _host: [None, 123, "not-an-ip"], + ) + + assert ok is False + assert "does not resolve to an IP" in reason + + +def test_resolver_skips_invalid_values_but_accepts_public_ip(): + ok, reason = check_outbound_url( + "https://example.test", + resolver=lambda _host: [None, "not-an-ip", "93.184.216.34"], + ) + + assert ok is True + assert reason == "ok"