diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 5f2921203ff..a50373f0ea8 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -20,7 +20,13 @@ import { Team } from "@/components/key_team_helpers/key_list"; import { MCPServers } from "@/components/mcp_tools"; import ModelHubTable from "@/components/AIHub/ModelHubTable"; import Navbar from "@/components/navbar"; -import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName, getInProductNudgesCall } from "@/components/networking"; +import { + getUiConfig, + Organization, + proxyBaseUrl, + setGlobalLitellmHeaderName, + getInProductNudgesCall, +} from "@/components/networking"; import NewUsagePage from "@/components/UsagePage/components/UsagePageView"; import OldTeams from "@/components/OldTeams"; import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button"; @@ -44,7 +50,13 @@ import SpendLogsTable from "@/components/view_logs"; import ViewUserDashboard from "@/components/view_users"; import { ThemeProvider } from "@/contexts/ThemeContext"; import { isJwtExpired } from "@/utils/jwtUtils"; -import { buildLoginUrlWithReturn, consumeReturnUrl, normalizeUrlForCompare, storeReturnUrl } from "@/utils/returnUrlUtils"; +import { + buildLoginUrlWithReturn, + consumeReturnUrl, + isValidReturnUrl, + normalizeUrlForCompare, + storeReturnUrl, +} from "@/utils/returnUrlUtils"; import { formatUserRole, isAdminRole } from "@/utils/roles"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { jwtDecode } from "jwt-decode"; @@ -133,15 +145,13 @@ function CreateKeyPageContent() { // Validate owned_by against allowed values const validOwnedByValues = ["you", "service_account", "another_user"]; - const validatedOwnedBy = ownedBy && validOwnedByValues.includes(ownedBy) - ? (ownedBy as CreateKeyPrefillData["owned_by"]) - : undefined; + const validatedOwnedBy = + ownedBy && validOwnedByValues.includes(ownedBy) ? (ownedBy as CreateKeyPrefillData["owned_by"]) : undefined; // Validate key_type against allowed values const validKeyTypes = ["default", "llm_api", "management"]; - const validatedKeyType = keyType && validKeyTypes.includes(keyType) - ? (keyType as CreateKeyPrefillData["key_type"]) - : undefined; + const validatedKeyType = + keyType && validKeyTypes.includes(keyType) ? (keyType as CreateKeyPrefillData["key_type"]) : undefined; // Sanitize key_alias (limit length, trim whitespace) const sanitizedKeyAlias = keyAlias @@ -153,8 +163,8 @@ function CreateKeyPageContent() { ? modelsParam .split(",") .slice(0, 100) // Limit number of models to prevent DoS - .map(m => m.trim().slice(0, 256)) // Limit individual model name length - .filter(m => m.length > 0) // Remove empty strings + .map((m) => m.trim().slice(0, 256)) // Limit individual model name length + .filter((m) => m.length > 0) // Remove empty strings : undefined; return { @@ -257,14 +267,16 @@ function CreateKeyPageContent() { // Check for a stored return URL const returnUrl = consumeReturnUrl(); - if (returnUrl) { + // Guard at redirect site so static analysis can verify the validation + + if (returnUrl && isValidReturnUrl(returnUrl)) { + const parsed = new URL(returnUrl, window.location.origin); + const safePath = parsed.pathname + parsed.search + parsed.hash; const currentUrl = window.location.href; - const normalizedReturnUrl = normalizeUrlForCompare(returnUrl); + const normalizedReturnUrl = normalizeUrlForCompare(safePath); const normalizedCurrentUrl = normalizeUrlForCompare(currentUrl); - // Only redirect if the return URL is different from the current URL - // This prevents infinite redirect loops if (normalizedReturnUrl !== normalizedCurrentUrl) { - window.location.replace(returnUrl); + window.location.replace(safePath); } } }, [authLoading, token]); @@ -433,234 +445,234 @@ function CreateKeyPageContent() { return ( }> - - - {invitation_id ? ( - + + {invitation_id ? ( + + ) : ( +
+ - ) : ( -
- -
-
+
+
- {page == "api-keys" ? ( - - ) : page == "models" ? ( - - ) : page == "llm-playground" ? ( - - ) : page == "users" ? ( - + ) : page == "models" ? ( + + ) : page == "llm-playground" ? ( + + ) : page == "users" ? ( + + ) : page == "teams" ? ( + + ) : page == "organizations" ? ( + + ) : page == "admin-panel" ? ( + + ) : page == "api_ref" ? ( + + ) : page == "logging-and-alerts" ? ( + + ) : page == "budgets" ? ( + + ) : page == "guardrails" ? ( + + ) : page == "policies" ? ( + + ) : page == "agents" ? ( + + ) : page == "prompts" ? ( + + ) : page == "transform-request" ? ( + + ) : page == "router-settings" ? ( + + ) : page == "ui-theme" ? ( + + ) : page == "cost-tracking" ? ( + + ) : page == "model-hub-table" ? ( + isAdminRole(userRole) ? ( + - ) : page == "teams" ? ( - - ) : page == "organizations" ? ( - - ) : page == "admin-panel" ? ( - - ) : page == "api_ref" ? ( - - ) : page == "logging-and-alerts" ? ( - - ) : page == "budgets" ? ( - - ) : page == "guardrails" ? ( - - ) : page == "policies" ? ( - - ) : page == "agents" ? ( - - ) : page == "prompts" ? ( - - ) : page == "transform-request" ? ( - - ) : page == "router-settings" ? ( - - ) : page == "ui-theme" ? ( - - ) : page == "cost-tracking" ? ( - - ) : page == "model-hub-table" ? ( - isAdminRole(userRole) ? ( - - ) : ( - - ) - ) : page == "caching" ? ( - - ) : page == "pass-through-settings" ? ( - - ) : page == "logs" ? ( - - ) : page == "mcp-servers" ? ( - - ) : page == "search-tools" ? ( - - ) : page == "tag-management" ? ( - - ) : page == "claude-code-plugins" ? ( - - ) : page == "access-groups" ? ( - - ) : page == "projects" ? ( - - ) : page == "vector-stores" ? ( - - ) : page == "tool-policies" ? ( - - ) : page == "guardrails-monitor" ? ( - - ) : page == "new_usage" ? ( - ) : ( - - )} -
- - {/* Survey Components */} - - - - {/* Claude Code Components */} - - + + ) + ) : page == "caching" ? ( + + ) : page == "pass-through-settings" ? ( + + ) : page == "logs" ? ( + + ) : page == "mcp-servers" ? ( + + ) : page == "search-tools" ? ( + + ) : page == "tag-management" ? ( + + ) : page == "claude-code-plugins" ? ( + + ) : page == "access-groups" ? ( + + ) : page == "projects" ? ( + + ) : page == "vector-stores" ? ( + + ) : page == "tool-policies" ? ( + + ) : page == "guardrails-monitor" ? ( + + ) : page == "new_usage" ? ( + + ) : ( + + )}
- )} - - + + {/* Survey Components */} + + + + {/* Claude Code Components */} + + +
+ )} + + ); } diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx index 822d9868392..38f6dcfe19f 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx @@ -7,13 +7,22 @@ import { MessageType, MultimodalContent } from "./types"; */ export const sanitizeImageSrc = (url: string | undefined): string => { if (!url) return ""; - if ( - url.startsWith("blob:") || - url.startsWith("data:") || - url.startsWith("http://") || - url.startsWith("https://") - ) { - return url; + try { + const parsed = new URL(url); + const proto = parsed.protocol; + if ( + proto === "blob:" || + proto === "data:" || + proto === "http:" || + proto === "https:" + ) { + // Return the reconstructed href so static-analysis taint from the + // original string is broken — parsed.href is built from parsed + // components, not the raw user-provided value. + return parsed.href; + } + } catch { + // invalid URL — fall through } return ""; };