#6315·odysseus

SSRF via model endpoint URL — no scheme/host validation on POST /model-endpoints

Author: isharak7mCreated Sep 16, 2026Updated Sep 16, 2026
Labelsbugready for review

Prerequisites

  • I searched open issues and discussions and did not find an existing report of this bug.
  • This is not a security vulnerability. (Vulnerabilities go to GitHub Security Advisories — see SECURITY.md.)
  • I am running the latest code from the dev branch (the default branch you get on clone, where fixes land first) and the bug still reproduces there. Please git pull the latest dev before filing.

Odysseus Revision

3b6c16916233 (2026-09-14)

Install Method

Manual Python install (pip / venv)

Operating System

Windows

Steps to Reproduce

  1. Start Odysseus with default config
  2. Authenticate as admin
  3. POST /api/model-endpoints/test with body: base_url=http://169.254.169.254/latest/meta-data/
  4. Server probes the cloud metadata endpoint and returns model details
  5. Repeat with base_url=file:///etc/passwd, base_url=http://10.0.0.1:6379
  6. All probes are accepted — no validation applied

Expected Behaviour

URLs resolving to link-local IPs (169.254.169.254), non-HTTP schemes (file://, gopher://), or loopback addresses should be rejected with HTTP 400, matching the existing SSRF protection pattern used in embedding_routes.py, contacts_routes.py, gallery_routes.py, and webhook routes — all of which call check_outbound_url() from src/url_safety.py.

Actual Behaviour

Both POST /api/model-endpoints/test and POST /api/model-endpoints accept arbitrary user-supplied URLs and make HTTP requests to them via _probe_endpoint() with zero validation — no scheme check, no DNS resolution, no private-IP filtering. The probe succeeds and returns endpoint/model details from the target URL.

Logs / Screenshots

Vulnerability confirmed on unfixed dev branch (3b6c16916233):

  Cloud metadata (169.254.169.254) → HTTP 200 (probe succeeds)
  file:///etc/passwd                → accepted, no rejection
  Internal Redis (10.0.0.1:6379)   → accepted, no rejection
  Loopback (127.0.0.1)             → accepted, no rejection

None return HTTP 400 — server accepts any URL and probes it.

AFTER PATCH (fix/model-endpoint-ssrf branch):
  test_cloud_metadata_blocked              PASSED
  test_non_http_scheme_blocked             PASSED
  test_loopback_always_rejected            PASSED
  test_strict_mode_blocks_loopback         PASSED
  test_cloud_metadata_blocked (create)     PASSED
  test_non_http_scheme_blocked (create)    PASSED
  test_loopback_always_rejected (create)   PASSED
  7 passed in 2.00s

Model / Backend (if relevant)

No response

Are you willing to submit a fix?

Yes — I can open a PR

Additional Information

Every other route that accepts external URLs in the codebase already uses check_outbound_url(): embedding (line 266), contacts (line 68), gallery (lines 335, 1272, 1536), notes (line 455), and webhooks (line 108). The model endpoint routes are the only two exceptions. A fix with 7 regression tests is ready on branch fix/model-endpoint-ssrf.