From cf6dce552efb479071d5d4428018b423d73e6c83 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 9 Apr 2026 16:58:12 +0800 Subject: [PATCH] Fix security and reliability issues 1. Validate URL scheme in open-external-url handler - Prevent opening file:// or other dangerous schemes via shell.openExternal - Only allow http:, https:, and mailto: protocols 2. Fix latest video detection using mtime instead of lexicographic sort - Lexicographic sort gives wrong results (e.g. recording-9 > recording-10) - Now sorts by file modification time for reliable latest-file detection 3. Add null guard for AudioData.format in cloneWithTimestamp - Replace non-null assertion (!) with proper validation - Throws descriptive error if format is unexpectedly null 4. Prevent encodeQueue counter underflow in VideoExporter - Use Math.max(0, ...) to prevent negative queue count Co-Authored-By: Claude Opus 4.6 --- electron/ipc/handlers.ts | 31 ++++++++++++++++++++++++++++++- src/lib/exporter/audioEncoder.ts | 7 +++++-- src/lib/exporter/videoExporter.ts | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 4cb4875..6aae4b4 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -490,7 +490,24 @@ export function registerIpcHandlers( return { success: false, message: "No recorded video found" }; } - const latestVideo = videoFiles.sort().reverse()[0]; + // Sort by most recently modified to reliably get the latest recording. + // Lexicographic sort is unreliable (e.g. recording-9.webm > recording-10.webm). + let latestVideo: string | null = null; + let latestMtimeMs = 0; + for (const file of videoFiles) { + try { + const stat = await fs.stat(path.join(RECORDINGS_DIR, file)); + if (stat.mtimeMs > latestMtimeMs) { + latestMtimeMs = stat.mtimeMs; + latestVideo = file; + } + } catch { + // Skip inaccessible files. + } + } + if (!latestVideo) { + return { success: false, message: "No recorded video found" }; + } const videoPath = path.join(RECORDINGS_DIR, latestVideo); return { success: true, path: videoPath }; @@ -616,6 +633,18 @@ export function registerIpcHandlers( ipcMain.handle("open-external-url", async (_, url: string) => { try { + const ALLOWED_SCHEMES = ["http:", "https:", "mailto:"]; + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return { success: false, error: "Invalid URL" }; + } + + if (!ALLOWED_SCHEMES.includes(parsed.protocol)) { + return { success: false, error: `Unsupported URL scheme: ${parsed.protocol}` }; + } + await shell.openExternal(url); return { success: true }; } catch (error) { diff --git a/src/lib/exporter/audioEncoder.ts b/src/lib/exporter/audioEncoder.ts index 490eed2..fba3568 100644 --- a/src/lib/exporter/audioEncoder.ts +++ b/src/lib/exporter/audioEncoder.ts @@ -459,7 +459,10 @@ export class AudioProcessor { } private cloneWithTimestamp(src: AudioData, newTimestamp: number): AudioData { - const isPlanar = src.format?.includes("planar") ?? false; + if (!src.format) { + throw new Error("AudioData format is required for cloning"); + } + const isPlanar = src.format.includes("planar"); const numPlanes = isPlanar ? src.numberOfChannels : 1; let totalSize = 0; @@ -476,7 +479,7 @@ export class AudioProcessor { } return new AudioData({ - format: src.format!, + format: src.format, sampleRate: src.sampleRate, numberOfFrames: src.numberOfFrames, numberOfChannels: src.numberOfChannels, diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index d0affd1..2c6ea8d 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -422,7 +422,7 @@ export class VideoExporter { })(); this.muxingPromises.push(muxingPromise); - this.encodeQueue--; + this.encodeQueue = Math.max(0, this.encodeQueue - 1); }, error: (error) => { console.error("[VideoExporter] Encoder error:", error);