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
+103 -10
View File
@@ -18,6 +18,11 @@
<el-icon><Back /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip v-if="isRemoteBrowser" content="复制远程选中文本">
<el-button size="small" text @click="copyRemoteSelection">
<el-icon><DocumentCopy /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip v-if="isRemoteBrowser" content="前进">
<el-button size="small" text @click="sendRemoteCommand('forward')">
<el-icon><Right /></el-icon>
@@ -67,7 +72,7 @@
class="remote-screen"
alt=""
draggable="false"
@load="() => { iframeLoading = false }"
@load="onRemoteImageLoad"
@error="() => handleRemoteSessionFailure(undefined, '远程浏览器截图加载失败')"
@pointerdown.stop.prevent="onRemotePointerDown"
@pointermove.stop.prevent="onRemotePointerMove"
@@ -126,7 +131,7 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, markRaw } f
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning,
ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning, DocumentCopy,
Link as LinkIcon, Monitor, SetUp, Reading, Cpu, DataLine,
Grid, Connection, Ticket, Wallet, Key, Tools, Star, House,
} from '@element-plus/icons-vue'
@@ -164,7 +169,12 @@ const isReconnectingRemoteBrowser = ref(false)
const remoteErrorState = ref<RemoteBrowserErrorState | null>(null)
let startRemoteBrowserPromise: Promise<void> | null = null
let screenshotObjectUrl = ''
let previousScreenshotObjectUrl = ''
let pendingScreenshotBlob: Blob | null = null
let screenshotFrameRequest: number | undefined
let mouseMoveTimer: number | undefined
let wheelTimer: number | undefined
let pendingWheel: { deltaX: number; deltaY: number; x: number; y: number } | null = null
let caretHideTimer: number | undefined
// Caret / cursor overlay
@@ -183,8 +193,11 @@ let wsReconnectTimer: number | undefined
let wsReconnectAttempts = 0
const WS_MAX_RECONNECT = 5
const WS_RECONNECT_BASE_MS = 800
const REMOTE_DRAG_MOVE_INTERVAL_MS = 16
const REMOTE_HOVER_MOVE_INTERVAL_MS = 80
const WS_BACKPRESSURE_BYTES = 256 * 1024
const REMOTE_DRAG_MOVE_INTERVAL_MS = 32
const REMOTE_HOVER_MOVE_INTERVAL_MS = 120
const REMOTE_WHEEL_INTERVAL_MS = 45
const HIGH_FREQUENCY_EVENTS = new Set(['mousemove', 'scroll'])
type RemoteBrowserErrorState = {
title: string
@@ -318,10 +331,7 @@ function connectRemoteWs() {
socket.onmessage = (evt) => {
if (evt.data instanceof Blob) {
// Binary frame = JPEG screenshot
const newUrl = URL.createObjectURL(evt.data)
setRemoteScreenshotUrl(newUrl)
iframeLoading.value = false
queueRemoteScreenshot(evt.data)
return
}
// Text frame = JSON control message
@@ -368,6 +378,11 @@ function stopRemoteWs() {
window.clearTimeout(wsReconnectTimer)
wsReconnectTimer = undefined
}
if (wheelTimer !== undefined) {
window.clearTimeout(wheelTimer)
wheelTimer = undefined
}
pendingWheel = null
if (ws) {
// Prevent onclose from triggering reconnect
const old = ws
@@ -380,11 +395,13 @@ function stopRemoteWs() {
async function sendRemoteEvent(payload: BrowserEventPayload) {
if (!props.active || isStartingRemoteBrowser.value) return
if (!remoteSession.value) return
const highFrequency = HIGH_FREQUENCY_EVENTS.has(payload.type)
if (ws && ws.readyState === WebSocket.OPEN) {
if (highFrequency && ws.bufferedAmount > WS_BACKPRESSURE_BYTES) return
ws.send(JSON.stringify(payload))
return
}
// Fallback: HTTP POST (e.g. during reconnect window)
if (highFrequency) return
try {
const res = await browserSessionsApi.event(remoteSession.value.id, payload)
remoteSession.value = res.data
@@ -397,6 +414,22 @@ function sendRemoteCommand(type: 'reload' | 'back' | 'forward') {
sendRemoteEvent({ type })
}
async function copyRemoteSelection() {
if (!remoteSession.value) return
try {
const res = await browserSessionsApi.selection(remoteSession.value.id)
const text = res.data.text.trim()
if (!text) {
ElMessage.warning('远程页面没有选中文本')
return
}
await navigator.clipboard.writeText(text)
ElMessage.success('已复制远程选中文本')
} catch (e: any) {
ElMessage.error(e.response?.data?.detail || '复制失败')
}
}
function remoteViewport() {
const rect = remoteFrameRef.value?.getBoundingClientRect()
return {
@@ -513,7 +546,23 @@ function onRemotePointerCancel(event: PointerEvent) {
function onRemoteWheel(event: WheelEvent) {
const point = eventPoint(event)
if (!point) return
sendRemoteEvent({ type: 'scroll', delta_x: event.deltaX, delta_y: event.deltaY, ...point })
if (!pendingWheel) {
pendingWheel = { deltaX: 0, deltaY: 0, ...point }
}
pendingWheel.deltaX += event.deltaX
pendingWheel.deltaY += event.deltaY
pendingWheel.x = point.x
pendingWheel.y = point.y
if (wheelTimer !== undefined) return
wheelTimer = window.setTimeout(flushRemoteWheel, REMOTE_WHEEL_INTERVAL_MS)
}
function flushRemoteWheel() {
wheelTimer = undefined
const wheel = pendingWheel
pendingWheel = null
if (!wheel) return
sendRemoteEvent({ type: 'scroll', delta_x: wheel.deltaX, delta_y: wheel.deltaY, x: wheel.x, y: wheel.y })
}
function onRemoteKeydown(event: KeyboardEvent) {
@@ -521,6 +570,12 @@ function onRemoteKeydown(event: KeyboardEvent) {
// which we handle in onRemotePaste with the actual clipboard text.
const isPaste = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v'
if (isPaste) return
const isCopy = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c'
if (isCopy) {
event.preventDefault()
copyRemoteSelection()
return
}
// Prevent browser from handling other keys (scrolling, shortcuts, etc.)
event.preventDefault()
@@ -568,7 +623,45 @@ async function closeRemoteSession() {
await browserSessionsApi.close(id).catch(() => undefined)
}
function queueRemoteScreenshot(blob: Blob) {
pendingScreenshotBlob = blob
if (screenshotFrameRequest !== undefined) return
screenshotFrameRequest = window.requestAnimationFrame(renderPendingScreenshot)
}
function renderPendingScreenshot() {
screenshotFrameRequest = undefined
const blob = pendingScreenshotBlob
pendingScreenshotBlob = null
if (!blob) return
const nextUrl = URL.createObjectURL(blob)
if (previousScreenshotObjectUrl) URL.revokeObjectURL(previousScreenshotObjectUrl)
previousScreenshotObjectUrl = screenshotObjectUrl
screenshotObjectUrl = nextUrl
remoteScreenshotUrl.value = nextUrl
}
function onRemoteImageLoad() {
iframeLoading.value = false
if (previousScreenshotObjectUrl) {
URL.revokeObjectURL(previousScreenshotObjectUrl)
previousScreenshotObjectUrl = ''
}
if (pendingScreenshotBlob && screenshotFrameRequest === undefined) {
screenshotFrameRequest = window.requestAnimationFrame(renderPendingScreenshot)
}
}
function setRemoteScreenshotUrl(url: string) {
if (screenshotFrameRequest !== undefined) {
window.cancelAnimationFrame(screenshotFrameRequest)
screenshotFrameRequest = undefined
}
pendingScreenshotBlob = null
if (previousScreenshotObjectUrl) {
URL.revokeObjectURL(previousScreenshotObjectUrl)
previousScreenshotObjectUrl = ''
}
if (screenshotObjectUrl) {
URL.revokeObjectURL(screenshotObjectUrl)
screenshotObjectUrl = ''