Coverage for src/local_deep_research/security/egress/fetch.py: 100%
12 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""Egress-aware URL fetch validation.
3A thin wrapper over the general SSRF validator that threads the egress
4*scope* through, so this layer (egress) depends on the SSRF validator rather
5than the SSRF validator depending on egress.
6"""
8from loguru import logger
10from ..ssrf_validator import validate_url
11from .policy import EgressScope
14def policy_aware_validate_url(url: str, egress_context=None) -> bool:
15 """Validate ``url`` for SSRF, taking the egress policy scope into account.
17 Under ``EgressScope.PRIVATE_ONLY`` the policy lets the user reach
18 private hosts (local lab deployments — Ollama on 127.0.0.1, SearXNG
19 on 192.168.x). Plain ``validate_url(url)`` would reject those at
20 the SSRF layer even though policy explicitly permits them. This
21 wrapper threads the scope through so a PRIVATE_ONLY run can reach
22 private hosts WITHOUT requiring the operator to set
23 ``SSRF_ALLOW_PRIVATE_IPS=1`` globally.
25 Cloud-metadata IPs in ``ALWAYS_BLOCKED_METADATA_IPS`` remain blocked
26 regardless of scope (handled inside ``is_ip_blocked``).
28 ``block_link_local=True`` is DEFENSE IN DEPTH, not a fix for a known
29 exploit. ``ALWAYS_BLOCKED_METADATA_IPS`` covers six literal metadata
30 addresses; the rest of 169.254.0.0/16 (and fe80::/10) is not a
31 deliberate service range, but it does host provider-specific metadata
32 endpoints outside those six -- Scaleway's 169.254.42.42, for one. A
33 PRIVATE_ONLY run is meant to reach a lab box on 192.168.x or
34 127.0.0.1, never an auto-configuration address, so excluding the
35 range costs nothing real and removes a class of target.
37 This also keeps the two egress gates consistent:
38 ``policy.py::_classify_host`` already passes ``block_link_local=True``,
39 and a validator pair that disagrees about what is reachable is its own
40 hazard -- whichever one a future caller happens to reach first then
41 decides the outcome.
43 When ``egress_context`` is ``None`` the call is equivalent to
44 ``validate_url(url)`` (strict defaults).
45 """
46 if egress_context is None:
47 return validate_url(url)
48 try:
49 if egress_context.scope == EgressScope.PRIVATE_ONLY:
50 return validate_url(
51 url, allow_private_ips=True, block_link_local=True
52 )
53 except Exception: # noqa: silent-exception
54 # Defensive: any attribute error here means the context is
55 # malformed; treat as if no policy was supplied and fall back to
56 # strict defaults. Logging is intentionally suppressed to avoid
57 # leaking policy state via timing.
58 logger.debug("policy_aware_validate_url defensive fallback")
59 return validate_url(url)