fix(ipc): confine recorded video writes

This commit is contained in:
wiiiii123
2026-07-10 05:24:53 +07:00
parent 641d2230a4
commit 682106972a
3 changed files with 80 additions and 2 deletions
@@ -0,0 +1,53 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveRecordedVideoStoragePath } from "./storagePath";
describe("resolveRecordedVideoStoragePath", () => {
const recordingsDir = path.resolve("recordings-root");
it.each([
"recording-0.webm",
"recording-1720588800000.mp4",
"recording-1720588800000-webcam.webm",
"recording-1720588800000-webcam.mp4",
])("accepts an app-generated recording name: %s", (fileName) => {
expect(resolveRecordedVideoStoragePath(recordingsDir, fileName)).toBe(
path.resolve(recordingsDir, fileName),
);
});
it.each([
"",
"../recording-1.webm",
"..\\recording-1.webm",
"nested/recording-1.webm",
"nested\\recording-1.webm",
"/tmp/recording-1.webm",
"C:\\temp\\recording-1.webm",
"\\\\server\\share\\recording-1.webm",
"recording-1.webm:payload",
"recording-1.webm\n",
"recording-1.webm\0",
"recording--1.webm",
"recording-1.5.webm",
"recording-1.mov",
"other-1.webm",
"recording-1-WEBCAM.webm",
])("rejects an untrusted recording name: %s", (fileName) => {
expect(() => resolveRecordedVideoStoragePath(recordingsDir, fileName)).toThrow(
"Invalid recording file name",
);
});
it.each([
{ label: "undefined", value: undefined },
{ label: "null", value: null },
{ label: "number", value: 1 },
{ label: "object", value: {} },
{ label: "array", value: [] },
])("rejects a non-string recording name: $label", ({ value }) => {
expect(() => resolveRecordedVideoStoragePath(recordingsDir, value)).toThrow(
"Invalid recording file name",
);
});
});
+24
View File
@@ -0,0 +1,24 @@
import path from "node:path";
const RECORDED_VIDEO_FILE_NAME = /^recording-[0-9]+(?:-webcam)?\.(?:webm|mp4)$/;
export function resolveRecordedVideoStoragePath(recordingsDir: string, fileName: unknown): string {
if (typeof fileName !== "string" || RECORDED_VIDEO_FILE_NAME.exec(fileName)?.[0] !== fileName) {
throw new Error("Invalid recording file name");
}
const resolvedRecordingsDir = path.resolve(recordingsDir);
const candidatePath = path.resolve(resolvedRecordingsDir, fileName);
const relativePath = path.relative(resolvedRecordingsDir, candidatePath);
if (
relativePath.length === 0 ||
relativePath === ".." ||
relativePath.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativePath)
) {
throw new Error("Invalid recording file name");
}
return candidatePath;
}
+3 -2
View File
@@ -68,6 +68,7 @@ import {
waitForNativeCaptureStart,
waitForNativeCaptureStop,
} from "../recording/mac";
import { resolveRecordedVideoStoragePath } from "../recording/storagePath";
import {
attachWindowsCaptureLifecycle,
isNativeWindowsCaptureAvailable,
@@ -1747,10 +1748,10 @@ export function registerRecordingHandlers(
},
);
ipcMain.handle("store-recorded-video", async (_, videoData: ArrayBuffer, fileName: string) => {
ipcMain.handle("store-recorded-video", async (_, videoData: ArrayBuffer, fileName: unknown) => {
try {
const recordingsDir = await getRecordingsDir();
const videoPath = path.join(recordingsDir, fileName);
const videoPath = resolveRecordedVideoStoragePath(recordingsDir, fileName);
await fs.writeFile(videoPath, Buffer.from(videoData));
return await finalizeStoredVideo(videoPath);
} catch (error) {