Files
odysseus/tests/test_url_safety.py
T
pewdiepie-archdaemonandClaude Opus 5.5 9bdea753ae fix(harness): ship coherent vendor harness cluster
Rebuild PR C to ship the vendor/photocraft harness as a coherent unit
(vendor implementation + vendor tests) instead of a per-file ahead/behind
mix that produced broken imports and 6+ test failures.

- Flip 36 harness impl/route/test files back to vendor (the per-file
  line-set policy misclassified user-rewritten files as 'behind').
- Flip core/models.py + src/tool_approval_scopes.py to vendor: vendor's
  harness (authority.py, turn_contract.py, clean_agent_preview.py) depends
  on vendor's older approval-scope API; upstream added
  CHAT_SESSION_APPROVAL_SIGNATURE_FIELD that vendor's core/models.py lacks.
- Add src/shutdown.py (new vendor infra) required by routes/chat_routes.py.
- Keep src/agent_tools/__init__.py at upstream (editor_tools registration
  lives in PR B).

Upstream-only security divergence preserved in C_upstream_delta.patch and
flagged for integration-time reconciliation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:59:04 +00:00

261 lines
10 KiB
Python

"""Tests for outbound URL safety / SSRF hardening (src/url_safety.py).
A stub resolver is injected so the tests never touch real DNS.
"""
from src.url_safety import check_outbound_url
def _resolver(mapping):
def resolve(host):
if host in mapping:
return mapping[host]
raise OSError(f"unresolvable: {host}")
return resolve
PUBLIC = _resolver({"example.com": ["93.184.216.34"]})
LOOPBACK = _resolver({"localhost": ["127.0.0.1"]})
LAN = _resolver({"nas.local": ["192.168.1.50"]})
METADATA = _resolver({"evil.example": ["169.254.169.254"]})
MAPPED_METADATA = _resolver({"evil6.example": ["::ffff:169.254.169.254"]})
def test_non_http_scheme_blocked():
for url in ("file:///etc/passwd", "ftp://x/y", "gopher://h", "redis://h:6379"):
ok, reason = check_outbound_url(url, resolver=PUBLIC)
assert ok is False, url
assert "scheme" in reason
def test_missing_host_or_empty_blocked():
assert check_outbound_url("", resolver=PUBLIC)[0] is False
assert check_outbound_url("http://", resolver=PUBLIC)[0] is False
def test_public_url_allowed():
ok, reason = check_outbound_url("https://example.com/v1/embeddings", resolver=PUBLIC)
assert ok is True, reason
def test_cloud_metadata_blocked_even_when_private_allowed():
# The headline SSRF vector must be blocked regardless of block_private.
ok, reason = check_outbound_url("http://evil.example/latest/meta-data/", resolver=METADATA)
assert ok is False
assert "link-local" in reason
def test_ipv4_mapped_metadata_blocked():
ok, reason = check_outbound_url("http://evil6.example/", resolver=MAPPED_METADATA)
assert ok is False
assert "link-local" in reason
def test_loopback_and_lan_allowed_by_default_local_first():
# Local-first: a localhost / LAN embedding server is a legitimate target.
assert check_outbound_url("http://localhost:8080/v1", resolver=LOOPBACK)[0] is True
assert check_outbound_url("http://nas.local:1234/v1", resolver=LAN)[0] is True
def test_strict_mode_blocks_private_and_loopback():
ok, reason = check_outbound_url("http://localhost:8080", block_private=True, resolver=LOOPBACK)
assert ok is False and "private" in reason
ok, reason = check_outbound_url("http://nas.local", block_private=True, resolver=LAN)
assert ok is False and "private" in reason
def test_strict_mode_blocks_cgnat_shared_space():
# RFC 6598 shared/CGNAT space (100.64.0.0/10) is not globally routable.
# A public redirect into it must be rejected under full SSRF lockdown,
# even though ipaddress reports is_private=False for this range.
CGNAT = _resolver({"svc.example": ["100.64.0.1"]})
ok, reason = check_outbound_url("http://svc.example:8080", block_private=True, resolver=CGNAT)
assert ok is False
assert "blocked" in reason
def test_strict_mode_blocks_non_global_ranges():
# Strict mode is a full SSRF lockdown: only globally-routable public
# addresses may be reached. Benchmarking (198.18.0.0/15) and TEST-NET
# documentation space (192.0.2.0/24) are not globally routable.
for ip in ("198.18.0.1", "192.0.2.10"):
res = _resolver({"svc.example": [ip]})
ok, reason = check_outbound_url("http://svc.example", block_private=True, resolver=res)
assert ok is False, ip
assert "blocked" in reason
def test_strict_mode_still_allows_public_ip():
# The lockdown must not reject a legitimate globally-routable target.
ok, reason = check_outbound_url("https://example.com/v1", block_private=True, resolver=PUBLIC)
assert ok is True, reason
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"
# --- RFC 6052 Well-Known Prefix (DNS64 / NAT64) ---------------------------
#
# On a DNS64/NAT64 network an IPv4-only host resolves to 64:ff9b::<v4>. The
# effective destination is the embedded IPv4 address, so that is what the
# policy must judge. RFC 6052 §3.1 permits the Well-Known Prefix to represent
# only globally-routable IPv4, so the embedded target is always held to the
# strict policy — the prefix must never tunnel past the SSRF guard.
def _nat64(v4: str) -> str:
"""Render the RFC 6052 Well-Known-Prefix form of an IPv4 address."""
import ipaddress
packed = int(ipaddress.IPv4Address(v4))
return str(ipaddress.IPv6Address(int(ipaddress.IPv6Address("64:ff9b::")) | packed))
def test_nat64_well_known_prefix_allows_public_ipv4_destination():
# The reproduced failure: export.arxiv.org resolves to 64:ff9b::924b:5b2a
# under DNS64. The embedded 146.75.91.42 is public, so the URL is allowed.
res = _resolver({"export.arxiv.org": [_nat64("146.75.91.42")]})
ok, reason = check_outbound_url("https://export.arxiv.org/api/query", resolver=res)
assert ok is True, reason
# ...including under full lockdown, where scholarly lookups actually run.
ok, reason = check_outbound_url(
"https://export.arxiv.org/api/query", block_private=True, resolver=res
)
assert ok is True, reason
def test_nat64_well_known_prefix_blocks_embedded_loopback():
res = _resolver({"evil.example": [_nat64("127.0.0.1")]})
for strict in (False, True):
ok, reason = check_outbound_url(
"http://evil.example/", block_private=strict, resolver=res
)
assert ok is False, strict
assert "NAT64" in reason and "127.0.0.1" in reason
def test_nat64_well_known_prefix_blocks_embedded_metadata_address():
# The headline SSRF vector must not become reachable through DNS64.
res = _resolver({"evil.example": [_nat64("169.254.169.254")]})
for strict in (False, True):
ok, reason = check_outbound_url(
"http://evil.example/latest/meta-data/", block_private=strict, resolver=res
)
assert ok is False, strict
assert "link-local" in reason and "169.254.169.254" in reason
def test_nat64_well_known_prefix_blocks_embedded_private_ipv4_under_strict():
res = _resolver({"evil.example": [_nat64("192.168.1.50")]})
ok, reason = check_outbound_url(
"http://evil.example/", block_private=True, resolver=res
)
assert ok is False
assert "NAT64" in reason and "192.168.1.50" in reason
def test_nat64_well_known_prefix_blocks_embedded_shared_space_under_strict():
# RFC 6598 shared/CGNAT space is not globally routable, so the Well-Known
# Prefix may not represent it.
res = _resolver({"evil.example": [_nat64("100.64.0.1")]})
ok, reason = check_outbound_url(
"http://evil.example/", block_private=True, resolver=res
)
assert ok is False
assert "NAT64" in reason and "100.64.0.1" in reason
def test_nat64_well_known_prefix_blocks_embedded_non_global_ipv4():
# Multicast, unspecified and other non-global space must not be reachable
# through the Well-Known Prefix, whatever block_private says.
for v4 in ("224.0.0.1", "0.0.0.0", "198.18.0.1", "192.0.2.10", "240.0.0.1"):
res = _resolver({"evil.example": [_nat64(v4)]})
for strict in (False, True):
ok, reason = check_outbound_url(
"http://evil.example/", block_private=strict, resolver=res
)
assert ok is False, (v4, strict)
assert v4 in reason, (v4, reason)
def test_network_specific_nat64_prefixes_are_not_decoded():
# RFC 8215 64:ff9b:1::/48 and arbitrary network-specific prefixes carry
# locally assigned semantics, so no embedded IPv4 may be inferred from them.
# 64:ff9b:1::8ef:5b2a would decode to the *public* 8.239.91.42; it stays
# rejected on the outer IPv6 address, which proves no decoding happened.
for addr in ("64:ff9b:1::8ef:5b2a", "64:ff9b:1::7f00:1"):
res = _resolver({"svc.example": [addr]})
ok, reason = check_outbound_url("http://svc.example/", resolver=res)
assert ok is False, addr
assert "NAT64" not in reason, addr
assert addr in reason, addr
# A documentation-range prefix is judged as an ordinary IPv6 address under
# the existing policy (local-first by default, blocked under lockdown) and
# is never reinterpreted as carrying an IPv4 destination.
res = _resolver({"svc.example": ["2001:db8:1::8ef:5b2a"]})
ok, reason = check_outbound_url("http://svc.example/", resolver=res)
assert ok is True, reason
ok, reason = check_outbound_url(
"http://svc.example/", block_private=True, resolver=res
)
assert ok is False
assert "NAT64" not in reason and "8.239.91.42" not in reason
def test_ordinary_ipv6_behaviour_is_unchanged():
# A global unicast IPv6 host is allowed in both modes.
GLOBAL6 = _resolver({"v6.example": ["2606:2800:220:1:248:1893:25c8:1946"]})
assert check_outbound_url("https://v6.example/", resolver=GLOBAL6)[0] is True
assert (
check_outbound_url("https://v6.example/", block_private=True, resolver=GLOBAL6)[0]
is True
)
# ULA is local-first allowed by default, blocked under lockdown.
ULA = _resolver({"ula.example": ["fd00::1"]})
assert check_outbound_url("http://ula.example/", resolver=ULA)[0] is True
ok, reason = check_outbound_url(
"http://ula.example/", block_private=True, resolver=ULA
)
assert ok is False and "private" in reason
# fe80::/10 is always blocked.
LL6 = _resolver({"ll.example": ["fe80::1"]})
ok, reason = check_outbound_url("http://ll.example/", resolver=LL6)
assert ok is False and "link-local" in reason
# Pre-existing behaviour, unchanged here: CPython reports ::1 as
# is_reserved, so IPv6 loopback is rejected in both modes (unlike 127.0.0.1,
# which the local-first default allows).
LOOP6 = _resolver({"loop6.example": ["::1"]})
for strict in (False, True):
ok, reason = check_outbound_url(
"http://loop6.example/", block_private=strict, resolver=LOOP6
)
assert ok is False, strict
assert "NAT64" not in reason