fix(SEC-WEB-30+31+36): Validate JSON.parse/localStorage deserialization
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Add runtime type validation after all JSON.parse calls in the web app to
prevent runtime crashes from corrupted or tampered storage data. Creates a
shared safeJsonParse utility with type guard functions for each data shape
(Message[], ChatOverlayState, LayoutConfigRecord). All four affected
callsites now validate parsed data and fall back to safe defaults on
mismatch.

Files changed:
- apps/web/src/lib/utils/safe-json.ts (new utility)
- apps/web/src/lib/utils/safe-json.test.ts (25 tests)
- apps/web/src/hooks/useChat.ts (deserializeMessages)
- apps/web/src/hooks/useChat.test.ts (3 new corruption tests)
- apps/web/src/hooks/useChatOverlay.ts (loadState)
- apps/web/src/hooks/useChatOverlay.test.ts (3 new corruption tests)
- apps/web/src/components/chat/ConversationSidebar.tsx (ideaToConversation)
- apps/web/src/lib/hooks/useLayout.ts (layout loading)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 15:46:58 -06:00
parent 6d92251fc1
commit 14b547d468
8 changed files with 516 additions and 22 deletions

View File

@@ -4,6 +4,7 @@
import { useCallback, useState, useEffect } from "react";
import type { WidgetPlacement, LayoutConfig } from "@mosaic/shared";
import { safeJsonParse, isLayoutConfigRecord } from "@/lib/utils/safe-json";
const STORAGE_KEY = "mosaic-layout";
const DEFAULT_LAYOUT_NAME = "default";
@@ -37,13 +38,14 @@ export function useLayout(): UseLayoutReturn {
const [currentLayoutId, setCurrentLayoutId] = useState<string>(DEFAULT_LAYOUT_NAME);
const [isLoading, setIsLoading] = useState(true);
// Load layouts from localStorage on mount
// Load layouts from localStorage on mount with runtime type validation
useEffect(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored) as Record<string, LayoutConfig>;
setLayouts(parsed);
const emptyFallback: Record<string, LayoutConfig> = {};
const parsed = safeJsonParse(stored, isLayoutConfigRecord, emptyFallback);
setLayouts(parsed as Record<string, LayoutConfig>);
}
// Load current layout ID preference