mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 07:12:20 +01:00
fix(security): block SSRF via user-supplied model endpoint URLs (#6316)
* fix: block SSRF via user-supplied model endpoint URLs * fix(test): stub DNS in loopback SSRF tests for deterministic results Replace environment-dependent test_loopback_always_rejected with two deterministic tests using a stub resolver: - test_loopback_accepted_by_default (local-first, block_private=False) - test_loopback_rejected_in_strict_mode (block_private=True) The old test only passed when localhost resolved to ::1 (hit is_reserved), not because loopback was actually always rejected. Co-Authored-By: Santoshkumarpuppala * fix: guard PATCH base_url against SSRF and add ipv6 loopback coverage --------- Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
This commit is contained in:
co-authored by
Santoshkumarpuppala
isharak7m
parent
b4ae7ebcc4
commit
0dbf457c00
@@ -47,6 +47,7 @@ from src.endpoint_resolver import (
|
||||
build_headers,
|
||||
)
|
||||
from src.auth_helpers import _auth_disabled, effective_user, owner_filter
|
||||
from src.url_safety import check_outbound_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2163,6 +2164,19 @@ def setup_model_routes(model_discovery):
|
||||
# keep those container-local when the frontend marks them as such.
|
||||
base_url = _rewrite_loopback_for_docker(base_url, container_local=_truthy(container_local))
|
||||
|
||||
# SSRF hardening: validate the user-supplied URL before any outbound
|
||||
# request. Local-first means loopback/LAN endpoints are allowed by
|
||||
# default; non-HTTP(S) schemes and the cloud metadata range are always
|
||||
# rejected. Set MODELENDPOINT_BLOCK_PRIVATE_IPS=true for full lockdown.
|
||||
# Note: the outbound fetches rely on httpx's default
|
||||
# follow_redirects=False — a redirect target is never dereferenced.
|
||||
ok, reason = check_outbound_url(
|
||||
base_url,
|
||||
block_private=os.getenv("MODELENDPOINT_BLOCK_PRIVATE_IPS", "false").lower() == "true",
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(400, f"Rejected endpoint URL: {reason}")
|
||||
|
||||
is_featherless = _host_match(base_url, "featherless.ai") or _safe_detect_provider(base_url) == "featherless"
|
||||
# Auto-generate name from URL if not provided
|
||||
if not name.strip():
|
||||
@@ -2399,6 +2413,16 @@ def setup_model_routes(model_discovery):
|
||||
from src.endpoint_resolver import resolve_url
|
||||
base_url = resolve_url(base_url)
|
||||
base_url = _rewrite_loopback_for_docker(base_url)
|
||||
# SSRF hardening: validate the user-supplied URL before any outbound
|
||||
# request. Local-first means loopback/LAN endpoints are allowed by
|
||||
# default; non-HTTP(S) schemes and the cloud metadata range are always
|
||||
# rejected. Set MODELENDPOINT_BLOCK_PRIVATE_IPS=true for full lockdown.
|
||||
ok, reason = check_outbound_url(
|
||||
base_url,
|
||||
block_private=os.getenv("MODELENDPOINT_BLOCK_PRIVATE_IPS", "false").lower() == "true",
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(400, f"Rejected endpoint URL: {reason}")
|
||||
requested_kind = _normalize_endpoint_kind(endpoint_kind)
|
||||
is_featherless = _host_match(base_url, "featherless.ai") or _safe_detect_provider(base_url) == "featherless"
|
||||
if is_featherless and requested_kind == "auto":
|
||||
@@ -2889,6 +2913,16 @@ def setup_model_routes(model_discovery):
|
||||
_new_base = _new_base[: -len(_suffix)].rstrip("/")
|
||||
_new_base = _normalize_base(_new_base)
|
||||
if _new_base:
|
||||
# SSRF hardening: PATCH rewrites the stored base_url, so
|
||||
# it needs the same guard as POST create/test — no
|
||||
# bypass through the update path. Follow-up fetches
|
||||
# rely on httpx's default follow_redirects=False.
|
||||
ok, reason = check_outbound_url(
|
||||
_new_base,
|
||||
block_private=os.getenv("MODELENDPOINT_BLOCK_PRIVATE_IPS", "false").lower() == "true",
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(400, f"Rejected endpoint URL: {reason}")
|
||||
ep.base_url = _new_base
|
||||
else:
|
||||
ep.is_enabled = not ep.is_enabled
|
||||
|
||||
Reference in New Issue
Block a user