security: fix #1 — wire SSRF hook into cache download client

Adds validate_public_request to the cache module's shared httpx
client event_hooks. Covers image/video/thumbnail downloads, which
are the most likely exfil path — file_url comes straight from the
booru JSON response and previously followed any 3xx that landed,
so a hostile booru could point downloads at a private IP. Every
redirect hop is now rejected if the target is non-public.

The import is lazy inside _get_shared_client because
core.api.base imports log_connection from this module; a top-level
`from .api._safety import ...` would circular-import through
api/__init__.py during cache.py load. By the time
_get_shared_client is called the api package is fully loaded.

Audit-Ref: SECURITY_AUDIT.md finding #1
Severity: High
This commit is contained in:
pax 2026-04-11 16:10:50 -05:00
parent 6eebb77ae5
commit ec79be9c83

View File

@ -79,6 +79,10 @@ def _get_shared_client(referer: str = "") -> httpx.AsyncClient:
c = _shared_client c = _shared_client
if c is not None and not c.is_closed: if c is not None and not c.is_closed:
return c return c
# Lazy import: core.api.base imports log_connection from this
# module, so a top-level `from .api._safety import ...` would
# circular-import through api/__init__.py during cache.py load.
from .api._safety import validate_public_request
with _shared_client_lock: with _shared_client_lock:
c = _shared_client c = _shared_client
if c is None or c.is_closed: if c is None or c.is_closed:
@ -89,6 +93,7 @@ def _get_shared_client(referer: str = "") -> httpx.AsyncClient:
}, },
follow_redirects=True, follow_redirects=True,
timeout=60.0, timeout=60.0,
event_hooks={"request": [validate_public_request]},
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
) )
_shared_client = c _shared_client = c