mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 03:06:22 +00:00
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user