diff --git a/assistant/src/daemon/handlers/recording.ts b/assistant/src/daemon/handlers/recording.ts index 8e6d2e4cdcc..ffb0433e37d 100644 --- a/assistant/src/daemon/handlers/recording.ts +++ b/assistant/src/daemon/handlers/recording.ts @@ -1,5 +1,5 @@ import * as net from 'node:net'; -import { existsSync, statSync } from 'node:fs'; +import { existsSync, realpathSync, statSync } from 'node:fs'; import * as path from 'node:path'; import { v4 as uuid } from 'uuid'; import type { RecordingStatus, RecordingOptions } from '../ipc-protocol.js'; @@ -202,13 +202,26 @@ function handleRecordingStatus( if (msg.filePath) { // Restrict accepted file paths to the app's recordings directory to // prevent attachment of arbitrary files via crafted IPC messages. - const resolvedPath = path.resolve(msg.filePath); + let resolvedPath: string; + try { + resolvedPath = realpathSync(msg.filePath); + } catch { + // File doesn't exist (broken symlink or missing) — use path.resolve + // as fallback; the existsSync check below will handle the missing file. + resolvedPath = path.resolve(msg.filePath); + } const allowedDir = path.join( process.env.HOME ?? '', 'Library/Application Support/vellum-assistant/recordings', ); - if (!resolvedPath.startsWith(allowedDir + path.sep) && resolvedPath !== allowedDir) { - log.warn({ recordingId, filePath: msg.filePath, allowedDir }, 'Recording file path outside allowed directory — rejecting'); + let resolvedAllowedDir: string; + try { + resolvedAllowedDir = realpathSync(allowedDir); + } catch { + resolvedAllowedDir = allowedDir; + } + if (!resolvedPath.startsWith(resolvedAllowedDir + path.sep) && resolvedPath !== resolvedAllowedDir) { + log.warn({ recordingId, filePath: msg.filePath, allowedDir, resolvedAllowedDir }, 'Recording file path outside allowed directory — rejecting'); if (notifySocket) { ctx.send(notifySocket, { type: 'assistant_text_delta',