mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix(ui): remove insecure ?token= URL handler from LoginPage to close session-fixation
This commit is contained in:
parent
256e05e474
commit
05a89b2c92
2 changed files with 81 additions and 15 deletions
|
|
@ -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<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
auto_redirect_to_sso: false,
|
||||
server_root_path: "/",
|
||||
proxy_base_url: null,
|
||||
sso_configured: false,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
(getCookie as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
(isJwtExpired as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
|
||||
const queryClient = createQueryClient();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LoginPage />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
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<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
auto_redirect_to_sso: false,
|
||||
server_root_path: "/",
|
||||
proxy_base_url: null,
|
||||
sso_configured: false,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
(getCookie as ReturnType<typeof vi.fn>).mockReturnValue("legitimate-session-jwt");
|
||||
(isJwtExpired as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
|
||||
const queryClient = createQueryClient();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LoginPage />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith("/ui");
|
||||
});
|
||||
|
||||
expect(document.cookie).not.toContain("token=attacker.jwt.value");
|
||||
expect(mockReplace).not.toHaveBeenCalledWith("/ui/?login=success");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue