mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
This commit is contained in:
parent
5c1583f852
commit
b4242d894d
@ -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')
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user