Skip to content

Commit f8307b6

Browse files
upendra-tessclaude
andcommitted
fix: guard localStorage access against restricted browser contexts (#331)
In private browsing, cross-origin iframes, or storage-blocked environments, calling localStorage.getItem/setItem throws a SecurityError. The existing typeof window === "undefined" guard does not cover this case. Introduce src/utils/safeStorage.ts with safeGetItem and safeSetItem helpers that wrap every localStorage call in try/catch and return a fallback value on failure. Apply across all affected call sites: - settings.tsx: sensitivity, authToken, invertScroll reads; all setItem calls - trackpad.tsx: sensitivity read (also adds NaN guard); rein_invert read (also wraps JSON.parse which can throw on malformed stored values) ConnectionProvider.tsx already had its own try/catch — left unchanged. __root.tsx typeof guard is preserved as-is. Fixes #331 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 1ae4791 commit f8307b6

File tree

3 files changed

+50
-10
lines changed

3 files changed

+50
-10
lines changed

src/routes/settings.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import QRCode from "qrcode"
33
import { useEffect, useState } from "react"
44
import { APP_CONFIG, THEMES } from "../config"
55
import serverConfig from "../server-config.json"
6+
import { safeGetItem, safeSetItem } from "../utils/safeStorage"
67

78
export const Route = createFileRoute("/settings")({
89
component: SettingsPage,
@@ -30,7 +31,7 @@ function SettingsPage() {
3031

3132
const [sensitivity, setSensitivity] = useState(() => {
3233
if (typeof window === "undefined") return 1.0
33-
const saved = localStorage.getItem("rein_sensitivity")
34+
const saved = safeGetItem("rein_sensitivity")
3435
const parsed = saved ? Number.parseFloat(saved) : Number.NaN
3536
return Number.isFinite(parsed) ? parsed : 1.0
3637
})
@@ -52,7 +53,7 @@ function SettingsPage() {
5253
// Load initial state (IP is not stored in localStorage; only sensitivity, invert, theme are client settings)
5354
const [authToken, setAuthToken] = useState(() => {
5455
if (typeof window === "undefined") return ""
55-
return localStorage.getItem("rein_auth_token") || ""
56+
return safeGetItem("rein_auth_token") || ""
5657
})
5758

5859
// Derive URLs once at the top
@@ -92,7 +93,7 @@ function SettingsPage() {
9293
if (data.type === "token-generated" && data.token) {
9394
if (isMounted) {
9495
setAuthToken(data.token)
95-
localStorage.setItem("rein_auth_token", data.token)
96+
safeSetItem("rein_auth_token", data.token)
9697
}
9798
socket.close()
9899
}
@@ -114,17 +115,17 @@ function SettingsPage() {
114115

115116
// Effect: Update LocalStorage when settings change
116117
useEffect(() => {
117-
localStorage.setItem("rein_sensitivity", String(sensitivity))
118+
safeSetItem("rein_sensitivity", String(sensitivity))
118119
}, [sensitivity])
119120

120121
useEffect(() => {
121-
localStorage.setItem("rein_invert", JSON.stringify(invertScroll))
122+
safeSetItem("rein_invert", JSON.stringify(invertScroll))
122123
}, [invertScroll])
123124

124125
// Effect: Theme
125126
useEffect(() => {
126127
if (typeof window === "undefined") return
127-
localStorage.setItem(APP_CONFIG.THEME_STORAGE_KEY, theme)
128+
safeSetItem(APP_CONFIG.THEME_STORAGE_KEY, theme)
128129
document.documentElement.setAttribute("data-theme", theme)
129130
}, [theme])
130131

src/routes/trackpad.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TouchArea } from "../components/Trackpad/TouchArea"
88
import { useRemoteConnection } from "../hooks/useRemoteConnection"
99
import { useTrackpadGesture } from "../hooks/useTrackpadGesture"
1010
import { ScreenMirror } from "../components/Trackpad/ScreenMirror"
11+
import { safeGetItem } from "../utils/safeStorage"
1112

1213
export const Route = createFileRoute("/trackpad")({
1314
component: TrackpadPage,
@@ -26,14 +27,19 @@ function TrackpadPage() {
2627
// Load Client Settings
2728
const [sensitivity] = useState(() => {
2829
if (typeof window === "undefined") return 1.0
29-
const s = localStorage.getItem("rein_sensitivity")
30-
return s ? Number.parseFloat(s) : 1.0
30+
const s = safeGetItem("rein_sensitivity")
31+
const parsed = s ? Number.parseFloat(s) : Number.NaN
32+
return Number.isFinite(parsed) ? parsed : 1.0
3133
})
3234

3335
const [invertScroll] = useState(() => {
3436
if (typeof window === "undefined") return false
35-
const s = localStorage.getItem("rein_invert")
36-
return s ? JSON.parse(s) : false
37+
try {
38+
const s = safeGetItem("rein_invert")
39+
return s ? JSON.parse(s) === true : false
40+
} catch {
41+
return false
42+
}
3743
})
3844

3945
const { send, sendCombo } = useRemoteConnection()

src/utils/safeStorage.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Safe localStorage wrappers that fall back gracefully in restricted browser
3+
* contexts (private browsing, cross-origin iframes, storage blocked by policy).
4+
*
5+
* In these environments, accessing `localStorage` throws a `SecurityError`
6+
* rather than returning null, so a simple `typeof localStorage` check is not
7+
* sufficient. These helpers catch any thrown error and return the provided
8+
* fallback value instead.
9+
*/
10+
11+
/**
12+
* Safely read a value from localStorage.
13+
* Returns `fallback` if storage is unavailable or the key does not exist.
14+
*/
15+
export function safeGetItem(key: string, fallback: string | null = null): string | null {
16+
try {
17+
return localStorage.getItem(key)
18+
} catch {
19+
return fallback
20+
}
21+
}
22+
23+
/**
24+
* Safely write a value to localStorage.
25+
* Silently does nothing if storage is unavailable.
26+
*/
27+
export function safeSetItem(key: string, value: string): void {
28+
try {
29+
localStorage.setItem(key, value)
30+
} catch {
31+
// Restricted context — ignore
32+
}
33+
}

0 commit comments

Comments
 (0)