From 77422aaaeb646c60be0c39b8b3eca97b6e139d10 Mon Sep 17 00:00:00 2001 From: meetp06 <65815199+meetp06@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:03:03 -0700 Subject: [PATCH 1/2] fix(ui): guard sessionStorage access against strict-privacy crash (#1453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare `sessionStorage` access throws a `SecurityError` in strict privacy modes (Safari Private Browsing, Chrome Incognito with third-party storage blocked, or when the UI is embedded in a sandboxed iframe). Because these calls run at module-eval time (clientSingleton) and during the initial React render (App.tsx), the unhandled throw aborts the render cycle and the user gets a blank white screen — a denial of service. Add a `safeSessionStorage` helper (try/catch wrappers that fall back to null / no-op) in each affected app and route the previously-unguarded sessionStorage calls through it: - apps/chat-ui/src/App.tsx - apps/dropper-ui/src/App.tsx - apps/dropper-ui/src/hooks/clientSingleton.ts - apps/shell-ui/src/util/pkce.ts The shell-ui auth providers, WorkspaceContext, connection.ts and Shell.tsx already wrap their sessionStorage access, so they are left unchanged. Co-Authored-By: Claude Opus 4.8 --- apps/chat-ui/src/App.tsx | 5 +- apps/chat-ui/src/utils/safeStorage.ts | 71 ++++++++++++++++++++ apps/dropper-ui/src/App.tsx | 5 +- apps/dropper-ui/src/hooks/clientSingleton.ts | 5 +- apps/dropper-ui/src/utils/safeStorage.ts | 71 ++++++++++++++++++++ apps/shell-ui/src/util/pkce.ts | 8 ++- apps/shell-ui/src/util/safeStorage.ts | 71 ++++++++++++++++++++ 7 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 apps/chat-ui/src/utils/safeStorage.ts create mode 100644 apps/dropper-ui/src/utils/safeStorage.ts create mode 100644 apps/shell-ui/src/util/safeStorage.ts diff --git a/apps/chat-ui/src/App.tsx b/apps/chat-ui/src/App.tsx index e20d028f0..7e5d6a2ad 100644 --- a/apps/chat-ui/src/App.tsx +++ b/apps/chat-ui/src/App.tsx @@ -28,6 +28,7 @@ import { VSCodeProvider, VSCodeContextType } from './hooks/useVSCode'; import { ChatContainer } from './components/ChatContainer'; import { API_CONFIG, setAPIConfig } from './config/apiConfig'; import { startClient } from './hooks/clientSingleton'; +import { safeSessionStorage } from './utils/safeStorage'; const App: React.FC = () => { const [isVSCode] = useState(() => 'acquireVsCodeApi' in window); @@ -99,7 +100,7 @@ const App: React.FC = () => { window.history.replaceState({}, '', window.location.pathname); } else if (!isVSCode) { // Fall back to session storage (skip in VSCode webview - shared storage would mix auth across tabs) - token = sessionStorage.getItem('auth') || ''; + token = safeSessionStorage.getItem('auth') || ''; } if (!token && API_CONFIG.devMode && API_CONFIG.ROCKETRIDE_APIKEY) { token = API_CONFIG.ROCKETRIDE_APIKEY; @@ -126,7 +127,7 @@ const App: React.FC = () => { // Save the token in session storage (skip in VSCode) and our state if (!isVSCode) { - sessionStorage.setItem('auth', token); + safeSessionStorage.setItem('auth', token); } setAuthToken(token); diff --git a/apps/chat-ui/src/utils/safeStorage.ts b/apps/chat-ui/src/utils/safeStorage.ts new file mode 100644 index 000000000..17be4e246 --- /dev/null +++ b/apps/chat-ui/src/utils/safeStorage.ts @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2026 Aparavi Software AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Safe `sessionStorage` accessors. + * + * Bare `sessionStorage` access throws a `SecurityError` in strict privacy + * modes (Safari Private Browsing, Chrome Incognito with third-party storage + * blocked, or when the UI is embedded in a sandboxed iframe). When that throw + * happens at module-eval time or during the initial React render, it aborts + * the whole render cycle and the user gets a blank white screen. + * + * These helpers swallow the exception and fall back to a harmless default so + * the app degrades to in-memory/limited functionality instead of crashing. + */ +export const safeSessionStorage = { + /** + * Read a value from `sessionStorage`, returning `null` if storage is + * unavailable or the key is missing. + */ + getItem(key: string): string | null { + try { + return sessionStorage.getItem(key); + } catch { + return null; + } + }, + + /** + * Write a value to `sessionStorage`. No-ops if storage is unavailable. + */ + setItem(key: string, value: string): void { + try { + sessionStorage.setItem(key, value); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, + + /** + * Remove a value from `sessionStorage`. No-ops if storage is unavailable. + */ + removeItem(key: string): void { + try { + sessionStorage.removeItem(key); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, +}; diff --git a/apps/dropper-ui/src/App.tsx b/apps/dropper-ui/src/App.tsx index 0e29531c5..a4ece9a53 100644 --- a/apps/dropper-ui/src/App.tsx +++ b/apps/dropper-ui/src/App.tsx @@ -10,6 +10,7 @@ import { VSCodeProvider, VSCodeContextType } from './hooks/useVSCode'; import { DropperContainer } from './components/DropperContainer'; import { API_CONFIG, setAPIConfig } from './config/apiConfig'; import { startClient } from './hooks/clientSingleton'; +import { safeSessionStorage } from './utils/safeStorage'; const App: React.FC = () => { const [isVSCode] = useState(() => 'acquireVsCodeApi' in window); @@ -71,7 +72,7 @@ const App: React.FC = () => { if (token) { window.history.replaceState({}, '', window.location.pathname); } else if (!isVSCode) { - token = sessionStorage.getItem('auth') || ''; + token = safeSessionStorage.getItem('auth') || ''; } if (!token && API_CONFIG.devMode && API_CONFIG.ROCKETRIDE_APIKEY) { token = API_CONFIG.ROCKETRIDE_APIKEY; @@ -94,7 +95,7 @@ const App: React.FC = () => { }); if (!isVSCode) { - sessionStorage.setItem('auth', token); + safeSessionStorage.setItem('auth', token); } setAuthToken(token); diff --git a/apps/dropper-ui/src/hooks/clientSingleton.ts b/apps/dropper-ui/src/hooks/clientSingleton.ts index a523c8467..c440be2b9 100644 --- a/apps/dropper-ui/src/hooks/clientSingleton.ts +++ b/apps/dropper-ui/src/hooks/clientSingleton.ts @@ -41,6 +41,7 @@ import { RocketRideClient, RocketRideClientConfig, ConnectionException } from 'rocketride'; import { API_CONFIG } from '../config/apiConfig'; +import { safeSessionStorage } from '../utils/safeStorage'; // ============================================================================ // SINGLETON STATE @@ -120,7 +121,7 @@ let lastConnectionError: { reason: string; hasError: boolean } | null = null; */ async function getAuthToken(): Promise { // Check session storage first for cached token - let auth = sessionStorage.getItem('auth'); + let auth = safeSessionStorage.getItem('auth'); if (auth) return auth; if (API_CONFIG.devMode) { @@ -150,7 +151,7 @@ async function getAuthToken(): Promise { } // Cache token in session storage for subsequent requests - if (auth) sessionStorage.setItem('auth', auth); + if (auth) safeSessionStorage.setItem('auth', auth); return auth; } diff --git a/apps/dropper-ui/src/utils/safeStorage.ts b/apps/dropper-ui/src/utils/safeStorage.ts new file mode 100644 index 000000000..17be4e246 --- /dev/null +++ b/apps/dropper-ui/src/utils/safeStorage.ts @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2026 Aparavi Software AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Safe `sessionStorage` accessors. + * + * Bare `sessionStorage` access throws a `SecurityError` in strict privacy + * modes (Safari Private Browsing, Chrome Incognito with third-party storage + * blocked, or when the UI is embedded in a sandboxed iframe). When that throw + * happens at module-eval time or during the initial React render, it aborts + * the whole render cycle and the user gets a blank white screen. + * + * These helpers swallow the exception and fall back to a harmless default so + * the app degrades to in-memory/limited functionality instead of crashing. + */ +export const safeSessionStorage = { + /** + * Read a value from `sessionStorage`, returning `null` if storage is + * unavailable or the key is missing. + */ + getItem(key: string): string | null { + try { + return sessionStorage.getItem(key); + } catch { + return null; + } + }, + + /** + * Write a value to `sessionStorage`. No-ops if storage is unavailable. + */ + setItem(key: string, value: string): void { + try { + sessionStorage.setItem(key, value); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, + + /** + * Remove a value from `sessionStorage`. No-ops if storage is unavailable. + */ + removeItem(key: string): void { + try { + sessionStorage.removeItem(key); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, +}; diff --git a/apps/shell-ui/src/util/pkce.ts b/apps/shell-ui/src/util/pkce.ts index 74c7b036c..7864b1955 100644 --- a/apps/shell-ui/src/util/pkce.ts +++ b/apps/shell-ui/src/util/pkce.ts @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { safeSessionStorage } from './safeStorage'; + // ============================================================================= // PKCE — Client-side OAuth PKCE utilities // Browser-only (uses crypto.subtle and fetch). @@ -114,7 +116,7 @@ export async function generatePkce(): Promise { // Persist the verifier in sessionStorage so it survives the browser // redirect to the Zitadel authorization endpoint and back. - sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier); + safeSessionStorage.setItem(PKCE_VERIFIER_KEY, verifier); return { verifier, challenge }; } @@ -132,7 +134,7 @@ export function getStoredVerifier(): string | null { // Read the previously stored verifier from sessionStorage. // Returns null if the key does not exist (e.g., the tab was closed // between the initial navigation and the redirect callback). - return sessionStorage.getItem(PKCE_VERIFIER_KEY); + return safeSessionStorage.getItem(PKCE_VERIFIER_KEY); } /** @@ -143,7 +145,7 @@ export function getStoredVerifier(): string | null { */ export function clearStoredVerifier(): void { // Remove the verifier entry from sessionStorage so it cannot be reused. - sessionStorage.removeItem(PKCE_VERIFIER_KEY); + safeSessionStorage.removeItem(PKCE_VERIFIER_KEY); } // ============================================================================= diff --git a/apps/shell-ui/src/util/safeStorage.ts b/apps/shell-ui/src/util/safeStorage.ts new file mode 100644 index 000000000..17be4e246 --- /dev/null +++ b/apps/shell-ui/src/util/safeStorage.ts @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2026 Aparavi Software AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Safe `sessionStorage` accessors. + * + * Bare `sessionStorage` access throws a `SecurityError` in strict privacy + * modes (Safari Private Browsing, Chrome Incognito with third-party storage + * blocked, or when the UI is embedded in a sandboxed iframe). When that throw + * happens at module-eval time or during the initial React render, it aborts + * the whole render cycle and the user gets a blank white screen. + * + * These helpers swallow the exception and fall back to a harmless default so + * the app degrades to in-memory/limited functionality instead of crashing. + */ +export const safeSessionStorage = { + /** + * Read a value from `sessionStorage`, returning `null` if storage is + * unavailable or the key is missing. + */ + getItem(key: string): string | null { + try { + return sessionStorage.getItem(key); + } catch { + return null; + } + }, + + /** + * Write a value to `sessionStorage`. No-ops if storage is unavailable. + */ + setItem(key: string, value: string): void { + try { + sessionStorage.setItem(key, value); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, + + /** + * Remove a value from `sessionStorage`. No-ops if storage is unavailable. + */ + removeItem(key: string): void { + try { + sessionStorage.removeItem(key); + } catch { + /* storage unavailable — degrade gracefully */ + } + }, +}; From 5ee8e7dae89eab634269fdc863226a5db7674a8e Mon Sep 17 00:00:00 2001 From: meetp06 <65815199+meetp06@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:31:02 -0700 Subject: [PATCH 2/2] docs(ui): cross-reference the duplicated safeSessionStorage copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback (CodeRabbit) on #1563: the safeSessionStorage helper is duplicated across chat-ui, dropper-ui and shell-ui. A shared workspace package is not a clean fit — chat-ui/dropper-ui do not depend on `shared` (packages/shared-ui) and the only common dependency, `rocketride`, is the public SDK client (wrong home for a browser-only UI util, and would expand its public surface). Rather than add cross-package dependencies, add a sync note to each copy so future fixes are applied in lockstep. Co-Authored-By: Claude Opus 4.8 --- apps/chat-ui/src/utils/safeStorage.ts | 7 +++++++ apps/dropper-ui/src/utils/safeStorage.ts | 7 +++++++ apps/shell-ui/src/util/safeStorage.ts | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/apps/chat-ui/src/utils/safeStorage.ts b/apps/chat-ui/src/utils/safeStorage.ts index 17be4e246..0275dd9b8 100644 --- a/apps/chat-ui/src/utils/safeStorage.ts +++ b/apps/chat-ui/src/utils/safeStorage.ts @@ -33,6 +33,13 @@ * * These helpers swallow the exception and fall back to a harmless default so * the app degrades to in-memory/limited functionality instead of crashing. + * + * NOTE: This helper is intentionally duplicated across the UI apps that do not + * share a common workspace package. Keep the copies in sync — apply any bug fix + * or new method (e.g. `clear`) to all three: + * - apps/chat-ui/src/utils/safeStorage.ts + * - apps/dropper-ui/src/utils/safeStorage.ts + * - apps/shell-ui/src/util/safeStorage.ts */ export const safeSessionStorage = { /** diff --git a/apps/dropper-ui/src/utils/safeStorage.ts b/apps/dropper-ui/src/utils/safeStorage.ts index 17be4e246..0275dd9b8 100644 --- a/apps/dropper-ui/src/utils/safeStorage.ts +++ b/apps/dropper-ui/src/utils/safeStorage.ts @@ -33,6 +33,13 @@ * * These helpers swallow the exception and fall back to a harmless default so * the app degrades to in-memory/limited functionality instead of crashing. + * + * NOTE: This helper is intentionally duplicated across the UI apps that do not + * share a common workspace package. Keep the copies in sync — apply any bug fix + * or new method (e.g. `clear`) to all three: + * - apps/chat-ui/src/utils/safeStorage.ts + * - apps/dropper-ui/src/utils/safeStorage.ts + * - apps/shell-ui/src/util/safeStorage.ts */ export const safeSessionStorage = { /** diff --git a/apps/shell-ui/src/util/safeStorage.ts b/apps/shell-ui/src/util/safeStorage.ts index 17be4e246..0275dd9b8 100644 --- a/apps/shell-ui/src/util/safeStorage.ts +++ b/apps/shell-ui/src/util/safeStorage.ts @@ -33,6 +33,13 @@ * * These helpers swallow the exception and fall back to a harmless default so * the app degrades to in-memory/limited functionality instead of crashing. + * + * NOTE: This helper is intentionally duplicated across the UI apps that do not + * share a common workspace package. Keep the copies in sync — apply any bug fix + * or new method (e.g. `clear`) to all three: + * - apps/chat-ui/src/utils/safeStorage.ts + * - apps/dropper-ui/src/utils/safeStorage.ts + * - apps/shell-ui/src/util/safeStorage.ts */ export const safeSessionStorage = { /**