From b4242d894dfd665efc3239adb62d9f8915294a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 10 May 2026 00:55:30 +0800 Subject: [PATCH] Prevent remote H5 tokens from leaking into local browser sessions Browser H5 startup now binds the in-memory bearer token only when the selected server URL is non-loopback, so stored remote credentials cannot follow a later localhost, 127.0.0.1, or IPv6 loopback startup. Token verification errors also use the structured HTTP status when available instead of relying only on message text. Constraint: Browser H5 may reuse localStorage across remote and local server URLs. Rejected: Clear all remembered H5 connection state on every loopback startup | preserving the remote URL is useful for the next remote retry and the token alone is the leak risk. Confidence: high Scope-risk: narrow Directive: Keep browser auth token assignment gated by requiresH5AuthForServerUrl before adding new bootstrap paths. Tested: cd desktop && bunx vitest run src/api/client.test.ts src/api/websocket.test.ts src/lib/desktopRuntime.test.ts src/components/layout/AppShell.test.tsx --- desktop/src/lib/desktopRuntime.test.ts | 18 +++++++++++++++++- desktop/src/lib/desktopRuntime.ts | 7 +++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/desktop/src/lib/desktopRuntime.test.ts b/desktop/src/lib/desktopRuntime.test.ts index 8bae06d6..023dcbab 100644 --- a/desktop/src/lib/desktopRuntime.test.ts +++ b/desktop/src/lib/desktopRuntime.test.ts @@ -56,7 +56,9 @@ describe('desktopRuntime browser H5 bootstrap', () => { globalThis.fetch = vi.fn().mockResolvedValue( new Response(null, { status: 200 }), ) as typeof fetch - clientMocks.postVerify.mockRejectedValueOnce(new Error('Unauthorized')) + clientMocks.postVerify.mockRejectedValueOnce( + Object.assign(new Error('Invalid or missing H5 access token'), { status: 401 }), + ) await expect(initializeDesktopServerUrl()).rejects.toMatchObject({ name: 'H5ConnectionRequiredError', @@ -70,6 +72,20 @@ describe('desktopRuntime browser H5 bootstrap', () => { expect(window.localStorage.getItem(H5_TOKEN_STORAGE_KEY)).toBeNull() }) + it('does not reuse stored remote H5 tokens for loopback browser startup', async () => { + window.history.pushState({}, '', '/?serverUrl=http%3A%2F%2F%5B%3A%3A1%5D%3A3456') + window.localStorage.setItem(H5_TOKEN_STORAGE_KEY, 'remote-token') + globalThis.fetch = vi.fn().mockResolvedValue( + new Response(null, { status: 200 }), + ) as typeof fetch + + await expect(initializeDesktopServerUrl()).resolves.toBe('http://[::1]:3456') + + expect(clientMocks.setBaseUrl).toHaveBeenLastCalledWith('http://[::1]:3456') + expect(clientMocks.setAuthToken).toHaveBeenLastCalledWith(null) + expect(clientMocks.postVerify).not.toHaveBeenCalled() + }) + it('normalizes unreachable remote browser startup into a recoverable H5 error', async () => { vi.useFakeTimers() window.history.pushState({}, '', '/?serverUrl=https%3A%2F%2Funreachable.example.com') diff --git a/desktop/src/lib/desktopRuntime.ts b/desktop/src/lib/desktopRuntime.ts index 77682689..22a1b079 100644 --- a/desktop/src/lib/desktopRuntime.ts +++ b/desktop/src/lib/desktopRuntime.ts @@ -135,7 +135,7 @@ async function initializeBrowserServerUrl(fallbackUrl: string) { const browserH5Runtime = requiresH5AuthForServerUrl(requestedUrl) setBaseUrl(requestedUrl) - setAuthToken(token) + setAuthToken(browserH5Runtime ? token : null) if (browserH5Runtime) { rememberStoredH5ServerUrl(requestedUrl) } @@ -247,7 +247,10 @@ function normalizeBrowserH5Error(error: unknown, serverUrl: string) { const message = error instanceof Error ? error.message : 'Unable to verify the H5 access token.' - const unauthorized = message.includes('401') || message.toLowerCase().includes('unauthorized') + const status = typeof error === 'object' && error !== null && 'status' in error + ? (error as { status?: unknown }).status + : undefined + const unauthorized = status === 401 || message.includes('401') || message.toLowerCase().includes('unauthorized') return new H5ConnectionRequiredError( unauthorized ? 'The saved H5 token is no longer valid.' : 'Unable to verify the H5 access token.', serverUrl,