diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx index 866b7d0f172..cd58c51a862 100644 --- a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx +++ b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx @@ -289,4 +289,85 @@ describe("LoginPage", () => { expect(ssoButton).toBeInTheDocument(); expect(ssoButton).toBeDisabled(); }); + + describe("URL ?token= legacy path is rejected (security regression test)", () => { + const originalLocation = window.location; + + beforeEach(() => { + Object.defineProperty(window, "location", { + value: { + ...originalLocation, + href: "http://localhost:3000/ui/login?token=attacker.jwt.value", + pathname: "/ui/login", + search: "?token=attacker.jwt.value", + }, + writable: true, + }); + document.cookie = + "token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax"; + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + value: originalLocation, + writable: true, + }); + }); + + it("must not set a token cookie or redirect to /ui/?login=success when ?token= is in the URL", async () => { + (useUIConfig as ReturnType).mockReturnValue({ + data: { + auto_redirect_to_sso: false, + server_root_path: "/", + proxy_base_url: null, + sso_configured: false, + }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(null); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument(); + }); + + expect(document.cookie).not.toContain("token=attacker.jwt.value"); + expect(mockReplace).not.toHaveBeenCalledWith("/ui/?login=success"); + }); + + it("must not overwrite an existing valid session cookie when ?token= is in the URL", async () => { + (useUIConfig as ReturnType).mockReturnValue({ + data: { + auto_redirect_to_sso: false, + server_root_path: "/", + proxy_base_url: null, + sso_configured: false, + }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue("legitimate-session-jwt"); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith("/ui"); + }); + + expect(document.cookie).not.toContain("token=attacker.jwt.value"); + expect(mockReplace).not.toHaveBeenCalledWith("/ui/?login=success"); + }); + }); }); diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx index 7ad3e32ef5c..74ee9f9de59 100644 --- a/ui/litellm-dashboard/src/app/login/LoginPage.tsx +++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx @@ -66,21 +66,6 @@ function LoginPageContent() { return; } - // Backwards compat: handle direct token in URL (legacy flow) - const urlToken = params.get("token"); - if (urlToken && !isJwtExpired(urlToken)) { - document.cookie = `token=${urlToken}; path=/; SameSite=Lax`; - params.delete("token"); - const cleanSearch = params.toString(); - window.history.replaceState( - null, - "", - window.location.pathname + (cleanSearch ? `?${cleanSearch}` : ""), - ); - router.replace("/ui/?login=success"); - return; - } - // If switching workers on a control plane, clear the old token and show login const switchingWorker = params.has("worker"); if (switchingWorker && uiConfig?.is_control_plane) {