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

@@ -320,6 +320,59 @@ describe("useChat", () => {
expect(result.current.conversationId).toBe("conv-123");
expect(result.current.conversationTitle).toBe("My Conversation");
});
it("should fall back to welcome message when stored JSON is corrupted", async () => {
vi.spyOn(console, "warn").mockImplementation(() => undefined);
mockGetIdea.mockResolvedValueOnce(
createMockIdea("conv-bad", "Corrupted", "not valid json {{{")
);
const { result } = renderHook(() => useChat());
await act(async () => {
await result.current.loadConversation("conv-bad");
});
// Should fall back to welcome message
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0]?.id).toBe("welcome");
});
it("should fall back to welcome message when stored data has wrong shape", async () => {
vi.spyOn(console, "warn").mockImplementation(() => undefined);
// Valid JSON but wrong shape (object instead of array, missing required fields)
mockGetIdea.mockResolvedValueOnce(
createMockIdea("conv-bad", "Wrong Shape", JSON.stringify({ not: "an array" }))
);
const { result } = renderHook(() => useChat());
await act(async () => {
await result.current.loadConversation("conv-bad");
});
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0]?.id).toBe("welcome");
});
it("should fall back to welcome message when messages have invalid roles", async () => {
vi.spyOn(console, "warn").mockImplementation(() => undefined);
const badMessages = [
{ id: "msg-1", role: "hacker", content: "Bad", createdAt: "2026-01-01" },
];
mockGetIdea.mockResolvedValueOnce(
createMockIdea("conv-bad", "Bad Roles", JSON.stringify(badMessages))
);
const { result } = renderHook(() => useChat());
await act(async () => {
await result.current.loadConversation("conv-bad");
});
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0]?.id).toBe("welcome");
});
});
describe("startNewConversation", () => {