Skip to content

Commit 7e920c0

Browse files
Copilothotlong
andcommitted
fix: add security validations - WebSocket URL protocol checks, localStorage JSON sanitization, fetch URL validation
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e4d75ee commit 7e920c0

4 files changed

Lines changed: 46 additions & 2 deletions

File tree

apps/console/src/components/AppSidebar.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,17 @@ function useNavOrder(appName: string) {
6565
const [orderMap, setOrderMap] = React.useState<Record<string, string[]>>(() => {
6666
try {
6767
const raw = localStorage.getItem(storageKey);
68-
return raw ? JSON.parse(raw) : {};
68+
if (!raw) return {};
69+
const parsed: unknown = JSON.parse(raw);
70+
// Validate shape: must be a plain object with string[] values
71+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {};
72+
const result: Record<string, string[]> = {};
73+
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
74+
if (Array.isArray(v) && v.every((i: unknown) => typeof i === 'string')) {
75+
result[k] = v as string[];
76+
}
77+
}
78+
return result;
6979
} catch {
7080
return {};
7181
}

packages/collaboration/src/useRealtimeSubscription.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ export function useRealtimeSubscription<T = unknown>(config: RealtimeConfig): Re
118118
return;
119119
}
120120

121+
// Validate WebSocket URL protocol (only ws: or wss: allowed)
122+
try {
123+
const parsed = new URL(url);
124+
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
125+
setError(new Error('WebSocket URL must use ws: or wss: protocol'));
126+
setConnectionState('error');
127+
return;
128+
}
129+
} catch {
130+
setError(new Error('Invalid WebSocket URL'));
131+
setConnectionState('error');
132+
return;
133+
}
134+
121135
// Close existing connection
122136
if (wsRef.current) {
123137
intentionalCloseRef.current = true;

packages/plugin-designer/src/CollaborationProvider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ export function CollaborationProvider({
7070
if (!canUseWS) return;
7171

7272
function connect() {
73-
const url = new URL(config.serverUrl!);
73+
// Validate WebSocket URL protocol (only ws: or wss: allowed)
74+
let url: URL;
75+
try {
76+
url = new URL(config.serverUrl!);
77+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return;
78+
} catch {
79+
return;
80+
}
7481
if (config.roomId) url.searchParams.set('room', config.roomId);
7582
url.searchParams.set('userId', user!.id);
7683

packages/react/src/hooks/useETagCache.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ export function useETagCache(userConfig: ETagCacheConfig = {}): ETagCacheResult
287287
url: string,
288288
options?: RequestInit,
289289
): Promise<{ data: T; fromCache: boolean; etag?: string }> => {
290+
// Validate URL: only allow http(s) or relative URLs
291+
if (url.includes('://')) {
292+
try {
293+
const parsed = new URL(url);
294+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
295+
throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
296+
}
297+
} catch (e) {
298+
if (e instanceof Error && e.message.startsWith('Unsupported')) throw e;
299+
throw new Error('Invalid URL');
300+
}
301+
}
302+
290303
if (!configRef.current.enabled) {
291304
const res = await fetch(url, options);
292305
const data = (await res.json()) as T;

0 commit comments

Comments
 (0)