From 51959feb89822eb7ec60b80294f91f60936811e5 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 27 Aug 2026 17:19:54 -0700 Subject: [PATCH] fix(ui): never treat a remote logo URL as a bundled asset The bundled-path check was an unanchored substring match, so a user-supplied logo URL that happened to carry /assets/logos/ or /_next/static/media/ in its path, and whose filename collided with one of the 36 manifest entries, would pick up a dark-mode treatment meant only for assets we ship. assetPaths already draws this line for resolveLogoSrc, which returns an external src untouched. Export that predicate instead of writing a second one, and require a treated src to clear it. The existing test only covered a remote URL with a bare filename, which passed either way. The new ones fail without the guard. --- .../src/components/molecules/logo/Logo.test.tsx | 5 +++++ ui/litellm-dashboard/src/lib/assetPaths.ts | 4 +++- ui/litellm-dashboard/src/lib/logoTreatments.test.ts | 11 +++++++++++ ui/litellm-dashboard/src/lib/logoTreatments.ts | 4 +++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx b/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx index 62ce8e9ee12..a8e7b4e41d4 100644 --- a/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx +++ b/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx @@ -81,6 +81,11 @@ describe("Logo", () => { expect(screen.getByRole("img", { name: "Ext logo" })).toHaveClass("w-5 h-5", { exact: true }); }); + it("does not treat a user-supplied logo URL that mimics the bundled asset path", () => { + render(); + expect(screen.getByRole("img", { name: "Ext logo" })).toHaveClass("w-5 h-5", { exact: true }); + }); + it("applies the treatment to a provider logo resolved through the bundler", () => { render(); expect(screen.getByRole("img", { name: "openrouter logo" })).toHaveClass("dark:[filter:brightness(0)_invert(1)]"); diff --git a/ui/litellm-dashboard/src/lib/assetPaths.ts b/ui/litellm-dashboard/src/lib/assetPaths.ts index abce69127cb..af208e7d78c 100644 --- a/ui/litellm-dashboard/src/lib/assetPaths.ts +++ b/ui/litellm-dashboard/src/lib/assetPaths.ts @@ -3,6 +3,8 @@ import { normalizeRootPath } from "@/lib/http/resolveApiBase"; const EXTERNAL_SRC = /^(https?:|data:|blob:|\/\/)/i; +export const isExternalAssetSrc = (value: string): boolean => EXTERNAL_SRC.test(value); + /** * Prefix a root-relative asset path (e.g. "/ui/assets/logos/openai.svg") with the * proxy's server root path so it resolves when the UI is mounted under a sub-path @@ -22,7 +24,7 @@ export const withServerRoot = (path: string, root: string): string => { */ export const resolveLogoSrc = (value: string | null | undefined, root: string = serverRootPath): string | undefined => { if (!value) return undefined; - if (EXTERNAL_SRC.test(value)) return value; + if (isExternalAssetSrc(value)) return value; if (value.includes("/_next/static/")) return value; const prefix = normalizeRootPath(root); if (prefix && (value === prefix || value.startsWith(`${prefix}/`))) return value; diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.test.ts b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts index b0a2073be8d..08ea283a7a3 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.test.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts @@ -35,6 +35,17 @@ describe("logoTreatmentFor", () => { expect(logoTreatmentFor("https://cdn.example.com/github.svg")).toBeUndefined(); }); + it("does not treat a remote URL that also carries a bundled-looking path", () => { + expect(logoTreatmentFor("https://cdn.example.com/assets/logos/github.svg")).toBeUndefined(); + expect(logoTreatmentFor("http://cdn.example.com/_next/static/media/github.abc123.svg")).toBeUndefined(); + expect(logoTreatmentFor("//cdn.example.com/assets/logos/github.svg")).toBeUndefined(); + expect(logoTreatmentFor("HTTPS://CDN.EXAMPLE.COM/assets/logos/github.svg")).toBeUndefined(); + }); + + it("does not treat a data URL that happens to contain a bundled-looking path", () => { + expect(logoTreatmentFor("data:image/svg+xml,/assets/logos/github.svg")).toBeUndefined(); + }); + it("does not treat a non-logo path whose filename collides with a bundled asset", () => { expect(logoTreatmentFor("/uploads/user/github.svg")).toBeUndefined(); }); diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.ts b/ui/litellm-dashboard/src/lib/logoTreatments.ts index f8378800046..7a5a7892d17 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.ts @@ -1,3 +1,5 @@ +import { isExternalAssetSrc } from "@/lib/assetPaths"; + export type LogoTreatment = "invert" | "plate"; const BUNDLED_LOGO_PATH = /(?:\/assets\/logos\/|\/_next\/static\/media\/)/; @@ -49,7 +51,7 @@ const withoutBundlerHash = (basename: string): string | undefined => { }; export const logoTreatmentFor = (src: string | null | undefined): LogoTreatment | undefined => { - if (!src || !BUNDLED_LOGO_PATH.test(src)) return undefined; + if (!src || isExternalAssetSrc(src) || !BUNDLED_LOGO_PATH.test(src)) return undefined; const basename = basenameOf(src); const key = basename === undefined ? undefined : withoutBundlerHash(basename); return key === undefined ? undefined : TREATMENT_BY_ASSET[key];