Skip to content

Commit 7777b52

Browse files
authored
fix(security): replace localStorage with sessionStorage for sensitive chat data
1 parent 6722ceb commit 7777b52

4 files changed

Lines changed: 30 additions & 8 deletions

File tree

apps/web/src/components/app-sidebar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,22 +265,22 @@ export function AppSidebar({
265265
useEffect(() => {
266266
if (typeof window === "undefined") return;
267267
try {
268-
const stored = localStorage.getItem(CHATS_CACHE_KEY);
268+
const stored = sessionStorage.getItem(CHATS_CACHE_KEY);
269269
if (stored && !cachedChatsRef.current) {
270270
cachedChatsRef.current = JSON.parse(stored);
271271
}
272272
} catch (e) {
273-
console.warn("Failed to load chats from localStorage:", e);
273+
console.warn("Failed to load chats from sessionStorage:", e);
274274
}
275275
}, []);
276276

277277
useEffect(() => {
278278
if (chatsResult?.chats && chatsResult.chats.length > 0) {
279279
cachedChatsRef.current = chatsResult.chats;
280280
try {
281-
localStorage.setItem(CHATS_CACHE_KEY, JSON.stringify(chatsResult.chats));
281+
sessionStorage.setItem(CHATS_CACHE_KEY, JSON.stringify(chatsResult.chats));
282282
} catch (e) {
283-
console.warn("Failed to save chats to localStorage:", e);
283+
console.warn("Failed to save chats to sessionStorage:", e);
284284
}
285285
}
286286
}, [chatsResult?.chats]);

apps/web/src/lib/auth-client.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,28 @@ export async function signUpWithEmail(
253253
});
254254
}
255255

256+
/**
257+
* Sensitive sessionStorage keys that store chat content, drafts, and stream data.
258+
* These must be cleared on sign-out to prevent data leakage on shared devices.
259+
*/
260+
const SENSITIVE_SESSION_KEYS = [
261+
"openchat-chats-cache",
262+
"openchat-prompt-drafts",
263+
"openchat-stream",
264+
];
265+
256266
export async function signOut() {
257267
return authClient.signOut({
258268
fetchOptions: {
259269
onSuccess: () => {
270+
// Clear sensitive chat/draft/stream data from sessionStorage
271+
for (const key of SENSITIVE_SESSION_KEYS) {
272+
try {
273+
sessionStorage.removeItem(key);
274+
} catch {
275+
// Ignore storage access errors
276+
}
277+
}
260278
window.location.href = "/auth/sign-in";
261279
},
262280
},

apps/web/src/stores/prompt-draft.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { create } from "zustand";
22
import { createJSONStorage, devtools, persist } from "zustand/middleware";
33

44
/**
5-
* Store for persisting prompt drafts across page reloads.
5+
* Store for persisting prompt drafts within the current browser session.
6+
*
7+
* Uses sessionStorage instead of localStorage to limit exposure of sensitive
8+
* chat content — data is scoped to the tab/session and not accessible after
9+
* the browser session ends.
610
*
711
* Non-annoying approach:
812
* - Drafts are saved per-chat (or "global" for new chat input)
913
* - Drafts are automatically cleared when a message is sent
10-
* - Old drafts are cleaned up after 7 days to prevent localStorage bloat
14+
* - Old drafts are cleaned up after 7 days to prevent storage bloat
1115
*/
1216

1317
const DRAFT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
@@ -115,7 +119,7 @@ export const usePromptDraftStore = create<PromptDraftState>()(
115119
}),
116120
{
117121
name: "openchat-prompt-drafts",
118-
storage: createJSONStorage(() => localStorage),
122+
storage: createJSONStorage(() => sessionStorage),
119123
},
120124
),
121125
{ name: "prompt-draft-store" },

apps/web/src/stores/stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export const useStreamStore = create<StreamState>()(
204204
}),
205205
{
206206
name: "openchat-stream",
207-
storage: createJSONStorage(() => localStorage),
207+
storage: createJSONStorage(() => sessionStorage),
208208
partialize: (state) => ({
209209
activeStream: state.activeStream,
210210
pendingUserMessage: state.pendingUserMessage,

0 commit comments

Comments
 (0)