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

1"""Egress-aware URL fetch validation. 

2 

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""" 

7 

8from loguru import logger 

9 

10from ..ssrf_validator import validate_url 

11from .policy import EgressScope 

12 

13 

14def policy_aware_validate_url(url: str, egress_context=None) -> bool: 

15 """Validate ``url`` for SSRF, taking the egress policy scope into account. 

16 

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. 

24 

25 Cloud-metadata IPs in ``ALWAYS_BLOCKED_METADATA_IPS`` remain blocked 

26 regardless of scope (handled inside ``is_ip_blocked``). 

27 

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. 

36 

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. 

42 

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)