mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 01:35:36 -04:00
fix: validate client-supplied image _endpoint to prevent SSRF (gallery proxies) (#1718)
POST /api/image/harmonize and POST /api/image/inpaint read an `_endpoint` from the request body and issue server-side httpx POSTs to it with no validation. A caller can set `_endpoint` to http://169.254.169.254/ (cloud instance metadata) or any internal/loopback address the server can reach, turning these routes into an SSRF primitive. routes/embedding_routes.py already runs its user-supplied endpoint through src.url_safety.check_outbound_url; these two routes were missing the same guard. Validate `_endpoint` the same way before any outbound request: non-HTTP(S) schemes and the link-local metadata range are always rejected, and IMAGE_BLOCK_PRIVATE_IPS=true blocks private/loopback for full lockdown (the local-first default still allows LAN diffusion servers). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -923,6 +923,16 @@ def setup_gallery_routes() -> APIRouter:
|
||||
body = await request.json()
|
||||
# Use endpoint from request body (editor dropdown) or fall back to DB lookup
|
||||
base = (body.pop("_endpoint", "") or "").rstrip("/")
|
||||
# SSRF hardening: validate a client-supplied endpoint before any
|
||||
# outbound request (mirrors routes/embedding_routes.py).
|
||||
if base:
|
||||
from src.url_safety import check_outbound_url
|
||||
ok, reason = check_outbound_url(
|
||||
base,
|
||||
block_private=os.getenv("IMAGE_BLOCK_PRIVATE_IPS", "false").lower() == "true",
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(400, f"Rejected endpoint URL: {reason}")
|
||||
chosen_model = (body.pop("_model", "") or "").strip()
|
||||
api_key = None
|
||||
if not base:
|
||||
@@ -1115,6 +1125,18 @@ def setup_gallery_routes() -> APIRouter:
|
||||
raise HTTPException(400, "No image provided")
|
||||
|
||||
endpoint = (body.get("_endpoint") or "").rstrip("/")
|
||||
# SSRF hardening: a client-supplied endpoint is fetched server-side
|
||||
# below, so validate it first (mirrors routes/embedding_routes.py).
|
||||
# Local-first means loopback/LAN is allowed by default; the cloud
|
||||
# metadata range and non-HTTP(S) schemes are always rejected.
|
||||
if endpoint:
|
||||
from src.url_safety import check_outbound_url
|
||||
ok, reason = check_outbound_url(
|
||||
endpoint,
|
||||
block_private=os.getenv("IMAGE_BLOCK_PRIVATE_IPS", "false").lower() == "true",
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(400, f"Rejected endpoint URL: {reason}")
|
||||
model = (body.get("_model") or "").strip()
|
||||
|
||||
base = endpoint
|
||||
|
||||
Reference in New Issue
Block a user