test(url-safety): cover IPv6 scope-ID strip + fail-closed in URL guards

Follow-up to the salvaged #25961 fix: regression tests asserting that
scope-bearing IPv6 addresses (fe80::1%eth0, ::1%lo) are blocked by
is_safe_url after the scope is stripped, that a still-unparseable address
fails closed, and that a scoped IPv4-mapped IMDS address is caught by the
always-blocked floor.
This commit is contained in:
teknium1 2026-06-21 13:38:32 -07:00 committed by Teknium
parent ed966696eb
commit 87ab373381

View file

@ -164,6 +164,31 @@ class TestIsSafeUrl:
]):
assert is_safe_url("http://[::ffff:169.254.169.254]/") is False
def test_ipv6_scope_id_link_local_blocked(self):
"""fe80::1%eth0 — a scope-ID-bearing link-local address must not bypass
the guard. ``ipaddress.ip_address`` rejects the ``%scope`` suffix, so
the scope must be stripped before the block check rather than skipped.
"""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("fe80::1%eth0", 0, 0, 0)),
]):
assert is_safe_url("http://[fe80::1%eth0]/") is False
def test_ipv6_scope_id_loopback_blocked(self):
"""::1%lo — scoped IPv6 loopback must still be blocked."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::1%lo", 0, 0, 0)),
]):
assert is_safe_url("http://[::1%lo]/") is False
def test_unparseable_ip_after_scope_strip_fails_closed(self):
"""An address that is still unparseable after stripping the scope ID
must fail closed (block), not be silently skipped."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("not-an-ip%garbage", 0, 0, 0)),
]):
assert is_safe_url("http://example.invalid/") is False
def test_unspecified_address_blocked(self):
"""0.0.0.0 — unspecified address, can bind to all interfaces."""
with patch("socket.getaddrinfo", return_value=[
@ -492,6 +517,15 @@ class TestIsAlwaysBlockedUrl:
]):
assert is_always_blocked_url("http://attacker-controlled.example.com/") is True
def test_scope_id_imds_in_floor_blocked(self):
"""A scope-ID suffix on an IPv4-mapped IMDS address resolving in the
always-blocked floor must be caught after the scope is stripped, not
skipped as unparseable."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::ffff:169.254.169.254%eth0", 0, 0, 0)),
]):
assert is_always_blocked_url("http://attacker-controlled.example.com/") is True
# -- Things the floor must NOT block ----------------------------------------
def test_public_url_not_blocked(self):