fix(media): restrict loopback approvals to supported files

This commit is contained in:
wiiiii123
2026-04-21 19:15:02 +07:00
parent c8c9386346
commit 7e87356cf8
4 changed files with 43 additions and 18 deletions
+13
View File
@@ -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);
});
});
+6
View File
@@ -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;
}
+1 -18
View File
@@ -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<string, string> = {
".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<string> | null = null;
function getMediaContentType(filePath: string): string {
return MEDIA_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
}
async function resolveRealPath(filePath: string): Promise<string | null> {
try {
return await fs.realpath(path.resolve(filePath));
+23
View File
@@ -0,0 +1,23 @@
import path from "node:path";
export const MEDIA_CONTENT_TYPES: Record<string, string> = {
".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;
}