From d5bad2f53eda7e884fa903b8f30d70831a0c6194 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 23 Mar 2026 23:06:42 -0700 Subject: [PATCH] address greptile review feedback (greploop iteration 1) - storageUtils: fix btoa() crash on non-Latin1 chars by encoding through encodeURIComponent before base64 - sanitizeImageSrc: restrict data: URIs to image/* and application/pdf - useMcpOAuthFlow: restore console.warn on storage failure, document localStorage migration fallback removal timeline - ChatUI: add comment explaining chatHistory persistence origin Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/playground/chat_ui/ChatUI.tsx | 4 ++++ .../components/playground/chat_ui/ResponsesImageUtils.tsx | 8 +++++++- ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx | 6 +++++- ui/litellm-dashboard/src/utils/storageUtils.ts | 7 +++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index c8886a56dbe..9f39425dc47 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -340,6 +340,10 @@ const ChatUI: React.FC = ({ proxySettings, ]); + // Note: this debounced chatHistory persistence was part of the original + // security-fixes branch (litellm_security_fixes_v1.82.3) and is ported + // as-is to keep the cherry-pick faithful. It prevents the synchronous + // sessionStorage writes that were flagged by the security scan. useEffect(() => { if (simplified) return; // Do not persist chat history in simplified (embedded) mode const handler = setTimeout(() => { 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 7b119036a90..6d327f41bf0 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx @@ -16,12 +16,18 @@ export const sanitizeImageSrc = (url: string | undefined): string => { const proto = parsed.protocol; if ( proto === "blob:" || - proto === "data:" || proto === "http:" || proto === "https:" ) { return parsed.href; } + // Restrict data: URIs to image and PDF MIME types only + if (proto === "data:") { + const mime = parsed.pathname.split(";")[0].toLowerCase(); + if (mime.startsWith("image/") || mime === "application/pdf") { + return parsed.href; + } + } } catch { // invalid URL — fall through } diff --git a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx index 4d12d615eb6..66571d0af80 100644 --- a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx +++ b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx @@ -83,13 +83,17 @@ export const useMcpOAuthFlow = ({ }; const getStorageItem = (key: string): string | null => { - // Try obfuscated sessionStorage first, fall back to legacy plain localStorage + // Try obfuscated sessionStorage first, fall back to legacy plain localStorage. + // The localStorage fallback exists for one-time migration of values written + // before the obfuscated-sessionStorage switch. It can be removed once all + // active sessions have been refreshed (target: next major release). const obfuscated = getObfuscated(key); if (obfuscated !== null) return obfuscated; if (typeof window === "undefined") return null; try { return window.localStorage.getItem(key); } catch (err) { + console.warn(`Failed to read legacy localStorage key "${key}"`, err); return null; } }; diff --git a/ui/litellm-dashboard/src/utils/storageUtils.ts b/ui/litellm-dashboard/src/utils/storageUtils.ts index 02af4ccc668..96e272a270f 100644 --- a/ui/litellm-dashboard/src/utils/storageUtils.ts +++ b/ui/litellm-dashboard/src/utils/storageUtils.ts @@ -10,7 +10,10 @@ export function setObfuscated(key: string, value: string): void { try { - sessionStorage.setItem(key, btoa(value)); + // Encode via encodeURIComponent first so non-Latin1 characters + // (e.g. Unicode MCP server aliases in OAuth flow-state JSON) + // are converted to percent-encoded ASCII before btoa. + sessionStorage.setItem(key, btoa(unescape(encodeURIComponent(value)))); } catch { // quota exceeded or SSR — silently drop } @@ -20,7 +23,7 @@ export function getObfuscated(key: string): string | null { try { const raw = sessionStorage.getItem(key); if (raw === null) return null; - return atob(raw); + return decodeURIComponent(escape(atob(raw))); } catch { // invalid base64 or SSR — treat as missing return null;