fix: address multiple code audit findings

- CORS: replace wildcard with explicit origin list from CORS_ORIGINS env
- Auth: enforce strong defaults, JWT blacklist (RevokedToken model), login rate limiting
- Auth: validate password length before bcrypt (72-byte limit)
- Scheduler: single-threaded worker to mitigate SQLite write contention
- Scheduler: graceful shutdown (wait=True)
- Snapshots: add prune_snapshots() with configurable retention count
- Storage: isolate localStorage keys via VITE_APP_KEY prefix
- Config: add cors_origins, login_rate_limit, snapshot_retention_count settings
This commit is contained in:
SmartUp Developer
2026-05-17 10:52:18 +08:00
parent a42ecf7bcc
commit ad16618406
25 changed files with 792 additions and 165 deletions
+34 -10
View File
@@ -41,6 +41,10 @@ class BrowserSessionResponse(BaseModel):
title: str
class BrowserSelectionResponse(BaseModel):
text: str
class BrowserEvent(BaseModel):
type: Literal["click", "dblclick", "mousemove", "mousedown", "mouseup", "type", "key", "scroll", "reload", "back", "forward", "resize"]
x: Optional[float] = None
@@ -119,6 +123,14 @@ async def send_event(session_id: str, body: BrowserEvent, _=Depends(get_current_
raise _error_from_browser(exc)
@router.get("/{session_id}/selection", response_model=BrowserSelectionResponse)
async def get_selection(session_id: str, _=Depends(get_current_user)):
try:
return BrowserSelectionResponse(text=await browser_sessions.selected_text(session_id))
except Exception as exc:
raise _error_from_browser(exc)
@router.delete("/{session_id}", status_code=204)
async def close_session(session_id: str, _=Depends(get_current_user)):
await browser_sessions.close(session_id)
@@ -126,9 +138,12 @@ async def close_session(session_id: str, _=Depends(get_current_user)):
# ——— WebSocket stream ———
# Frame interval & diff detection
_WS_MIN_INTERVAL = 0.05 # 50 ms floor (≈20 fps max)
_WS_IDLE_INTERVAL = 0.15 # 150 ms when nothing changed recently
_WS_ACTIVE_INTERVAL = 0.08 # 80 ms right after a user event
_WS_MIN_INTERVAL = 0.10
_WS_IDLE_INTERVAL = 0.35
_WS_ACTIVE_INTERVAL = 0.12
_WS_BACKOFF_INTERVAL = 0.60
_WS_DEEP_IDLE_INTERVAL = 1.00
_WS_ACTIVE_WINDOW = 1.25
async def _ws_authenticate(token: Optional[str]) -> bool:
@@ -163,10 +178,11 @@ async def session_ws(
# Track when a user event arrived so we can temporarily speed up
last_event_at: float = 0.0
last_frame_hash: str = ""
unchanged_count = 0
# Task: receive events from client
async def receive_loop():
nonlocal last_event_at
nonlocal last_event_at, unchanged_count
try:
while True:
raw = await websocket.receive_text()
@@ -179,8 +195,9 @@ async def session_ws(
continue
payload: dict[str, Any] = {k: v for k, v in msg.items() if k != "type"}
try:
await browser_sessions.event(session_id, msg_type, payload)
await browser_sessions.event(session_id, msg_type, payload, include_state=False)
last_event_at = asyncio.get_event_loop().time()
unchanged_count = 0
except Exception as exc:
logger.warning("ws event error: %s", exc)
try:
@@ -194,17 +211,22 @@ async def session_ws(
# Task: push screenshots
async def push_loop():
nonlocal last_frame_hash
nonlocal last_frame_hash, unchanged_count
try:
while True:
now = asyncio.get_event_loop().time()
# Faster cadence right after a user interaction
interval = _WS_ACTIVE_INTERVAL if (now - last_event_at) < 1.0 else _WS_IDLE_INTERVAL
if (now - last_event_at) < _WS_ACTIVE_WINDOW:
interval = _WS_ACTIVE_INTERVAL
elif unchanged_count >= 9:
interval = _WS_DEEP_IDLE_INTERVAL
elif unchanged_count >= 3:
interval = _WS_BACKOFF_INTERVAL
else:
interval = _WS_IDLE_INTERVAL
try:
frame = await browser_sessions.screenshot(session_id)
except KeyError:
# Session gone
await websocket.send_json({"error": "session_not_found"})
break
except Exception as exc:
@@ -212,14 +234,16 @@ async def session_ws(
await asyncio.sleep(interval)
continue
# Only push if content changed
frame_hash = hashlib.md5(frame).hexdigest()
if frame_hash != last_frame_hash:
last_frame_hash = frame_hash
unchanged_count = 0
try:
await websocket.send_bytes(frame)
except Exception:
break
else:
unchanged_count += 1
await asyncio.sleep(max(_WS_MIN_INTERVAL, interval))
except (WebSocketDisconnect, asyncio.CancelledError):