diff --git a/electron/ipc/project/manager.test.ts b/electron/ipc/project/manager.test.ts index 33606541..0236b55b 100644 --- a/electron/ipc/project/manager.test.ts +++ b/electron/ipc/project/manager.test.ts @@ -87,4 +87,17 @@ describe("local media path policy", () => { await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBe(videoPath); expect(isAllowedMediaPath(videoPath)).toBe(true); }); + + it("rejects existing non-media files when resolving local media URLs", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const textPath = path.join(downloadsPath, "notes.txt"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(textPath, "not media"); + + const { resolveApprovedLocalMediaPath } = await import("./manager"); + const { isAllowedMediaPath } = await import("../../mediaServer"); + + await expect(resolveApprovedLocalMediaPath(textPath)).resolves.toBeNull(); + expect(isAllowedMediaPath(textPath)).toBe(false); + }); }); diff --git a/electron/ipc/project/manager.ts b/electron/ipc/project/manager.ts index 67e3dead..473f3a63 100644 --- a/electron/ipc/project/manager.ts +++ b/electron/ipc/project/manager.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { app } from "electron"; import { RECORDINGS_DIR, USER_DATA_PATH } from "../../appPaths"; +import { isSupportedLocalMediaPath } from "../../mediaTypes"; import { PROJECT_FILE_EXTENSION, LEGACY_PROJECT_FILE_EXTENSIONS, @@ -91,6 +92,11 @@ export async function resolveApprovedLocalMediaPath(candidatePath: string): Prom return null; } + const stat = await fs.stat(realPath).catch(() => null); + if (!stat?.isFile() || !isSupportedLocalMediaPath(realPath)) { + return null; + } + if (!(await isAllowedLocalMediaPath(realPath))) { return null; } diff --git a/electron/mediaServer.ts b/electron/mediaServer.ts index ff783788..201e58aa 100644 --- a/electron/mediaServer.ts +++ b/electron/mediaServer.ts @@ -3,28 +3,11 @@ import fs from "node:fs/promises"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import path from "node:path"; import { approvedLocalReadPaths } from "./ipc/state"; - -const MEDIA_MIME_TYPES: Record = { - ".mp4": "video/mp4", - ".webm": "video/webm", - ".mov": "video/quicktime", - ".mkv": "video/x-matroska", - ".avi": "video/x-msvideo", - ".wav": "audio/wav", - ".mp3": "audio/mpeg", - ".ogg": "audio/ogg", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", -}; +import { getMediaContentType } from "./mediaTypes"; let mediaServerBaseUrl: string | null = null; let mediaServerStartPromise: Promise | null = null; -function getMediaContentType(filePath: string): string { - return MEDIA_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"; -} - async function resolveRealPath(filePath: string): Promise { try { return await fs.realpath(path.resolve(filePath)); diff --git a/electron/mediaTypes.ts b/electron/mediaTypes.ts new file mode 100644 index 00000000..5c4307b6 --- /dev/null +++ b/electron/mediaTypes.ts @@ -0,0 +1,23 @@ +import path from "node:path"; + +export const MEDIA_CONTENT_TYPES: Record = { + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mov": "video/quicktime", + ".mkv": "video/x-matroska", + ".avi": "video/x-msvideo", + ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".ogg": "audio/ogg", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", +}; + +export function getMediaContentType(filePath: string): string { + return MEDIA_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"; +} + +export function isSupportedLocalMediaPath(filePath: string): boolean { + return path.extname(filePath).toLowerCase() in MEDIA_CONTENT_TYPES; +}