fix: timing-safe key comparison and network-error state in liveview
Use hmac.compare_digest() to prevent timing-based key enumeration. Distinguish network failures from invalid-key errors in the frontend.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import hmac
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
@@ -26,7 +27,7 @@ async def liveview_canvas(
|
||||
"""
|
||||
if not settings.liveview_key:
|
||||
raise HTTPException(status_code=403, detail="Live view is disabled")
|
||||
if not key or key != settings.liveview_key:
|
||||
if not key or not hmac.compare_digest(key, settings.liveview_key):
|
||||
raise HTTPException(status_code=403, detail="Invalid live view key")
|
||||
|
||||
nodes = (await db.execute(select(Node))).scalars().all()
|
||||
|
||||
@@ -33,7 +33,7 @@ import type { NodeData } from '@/types'
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
const STORAGE_KEY = 'homelable_canvas'
|
||||
|
||||
type ViewState = 'loading' | 'disabled' | 'invalid-key' | 'no-key' | 'ready'
|
||||
type ViewState = 'loading' | 'disabled' | 'invalid-key' | 'no-key' | 'network-error' | 'ready'
|
||||
|
||||
function LiveViewCanvas() {
|
||||
const { nodes, edges, loadCanvas } = useCanvasStore()
|
||||
@@ -81,7 +81,8 @@ function LiveViewCanvas() {
|
||||
setViewState('ready')
|
||||
})
|
||||
.catch((err) => {
|
||||
const detail: string = err.response?.data?.detail ?? ''
|
||||
if (!err.response) { setViewState('network-error'); return }
|
||||
const detail: string = err.response.data?.detail ?? ''
|
||||
setViewState(detail === 'Live view is disabled' ? 'disabled' : 'invalid-key')
|
||||
})
|
||||
}, [loadCanvas])
|
||||
@@ -104,6 +105,7 @@ function LiveViewCanvas() {
|
||||
disabled: 'Live view is disabled on this instance.',
|
||||
'invalid-key': 'Invalid or expired live view key.',
|
||||
'no-key': 'Missing key — use ?key=your-secret in the URL.',
|
||||
'network-error': 'Could not reach the server. Check your connection.',
|
||||
}
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-[#0d1117]">
|
||||
|
||||
@@ -88,12 +88,12 @@ describe('LiveView (non-standalone)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows invalid-key error for unexpected API errors', async () => {
|
||||
it('shows network-error for non-response errors (offline, CORS, 500)', async () => {
|
||||
setSearch('?key=anything')
|
||||
vi.mocked(liveviewApi.load).mockRejectedValue(new Error('network'))
|
||||
render(<LiveView />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Invalid or expired/)).toBeDefined()
|
||||
expect(screen.getByText(/Could not reach the server/)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user