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) <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-23 23:06:42 -07:00
parent 065e7b45e1
commit d5bad2f53e
4 changed files with 21 additions and 4 deletions

View file

@ -340,6 +340,10 @@ const ChatUI: React.FC<ChatUIProps> = ({
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(() => {

View file

@ -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
}

View file

@ -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;
}
};

View file

@ -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;