fix(proxy): /get_logo_url no longer discloses local UI_LOGO_PATH

The unauthenticated ``/get_logo_url`` endpoint returned the
``UI_LOGO_PATH`` env var verbatim. For HTTP(S) URLs this is intended —
the dashboard loads the logo directly from a public/internal CDN. For
local filesystem paths it was an information disclosure: any caller
could fetch ``/get_logo_url`` and read admin-only filesystem details
like ``UI_LOGO_PATH=/etc/litellm/secret-config.json``.

Now the endpoint returns the URL only when it begins with
``http://`` or ``https://``. For local paths (or unset) it returns an
empty string — the dashboard falls back to ``/get_image`` which
serves the file via the path-containment guard added in the previous
commit.

Tests parametrize the disclosure-blocked cases (``/etc/...``,
``/proc/self/environ``, relative paths) and confirm HTTP / HTTPS URLs
still pass through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
user
2026-04-29 21:15:21 +00:00
parent 0166992f6b
commit 9ef8572d67
2 changed files with 67 additions and 2 deletions
+13 -2
View File
@@ -12223,9 +12223,20 @@ async def claim_onboarding_link(data: InvitationClaim):
@app.get("/get_logo_url", include_in_schema=False)
def get_logo_url():
"""Get the current logo URL from environment"""
"""Get the current logo URL from environment.
Only HTTP(S) URLs are returned those are intended to be loaded
directly by the browser from a public/internal CDN. Local file
paths set via ``UI_LOGO_PATH`` are NOT returned: they are admin-
only filesystem details, the dashboard falls back to ``/get_image``
which serves the file (with path containment) instead. Without
this filter, the unauthenticated endpoint would disclose internal
hostnames or filesystem paths to any caller.
"""
logo_path = os.getenv("UI_LOGO_PATH", "")
return {"logo_url": logo_path}
if logo_path.startswith(("http://", "https://")):
return {"logo_url": logo_path}
return {"logo_url": ""}
@app.get("/get_image", include_in_schema=False)
@@ -457,6 +457,60 @@ def test_fallback_login_has_no_deprecation_banner(client_no_auth):
assert "<form" in html
@pytest.mark.parametrize(
"ui_logo_path",
[
"/etc/litellm/secret-config.json",
"/var/secrets/admin.key",
"/proc/self/environ",
"relative/path/logo.png",
],
)
def test_get_logo_url_does_not_disclose_local_paths(
client_no_auth, monkeypatch, ui_logo_path
):
# ``/get_logo_url`` is unauthenticated. Returning a local filesystem
# path verbatim discloses admin-only config to any caller. Only
# browser-loadable HTTP(S) URLs should be returned; for local paths
# the dashboard falls back to ``/get_image`` (which has path
# containment).
monkeypatch.setenv("UI_LOGO_PATH", ui_logo_path)
response = client_no_auth.get("/get_logo_url")
assert response.status_code == 200
assert response.json() == {"logo_url": ""}
def test_get_logo_url_returns_https_url(client_no_auth, monkeypatch):
monkeypatch.setenv("UI_LOGO_PATH", "https://cdn.public.example/logo.png")
response = client_no_auth.get("/get_logo_url")
assert response.status_code == 200
assert response.json() == {"logo_url": "https://cdn.public.example/logo.png"}
def test_get_logo_url_returns_http_url(client_no_auth, monkeypatch):
# HTTP URLs (typically internal CDN) are still returned — those are
# intended to be loaded directly by the browser.
monkeypatch.setenv("UI_LOGO_PATH", "http://internal-cdn.corp:8080/logo.png")
response = client_no_auth.get("/get_logo_url")
assert response.status_code == 200
assert response.json() == {"logo_url": "http://internal-cdn.corp:8080/logo.png"}
def test_get_logo_url_returns_empty_when_unset(client_no_auth, monkeypatch):
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client_no_auth.get("/get_logo_url")
assert response.status_code == 200
assert response.json() == {"logo_url": ""}
def test_sso_key_generate_shows_deprecation_banner(client_no_auth, monkeypatch):
# Ensure the route returns the HTML form instead of redirecting
monkeypatch.setattr(