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 <noreply@anthropic.com>
This commit is contained in:
Test User
2026-04-09 16:58:12 +08:00
parent e7d5f51740
commit cf6dce552e
3 changed files with 36 additions and 4 deletions
+30 -1
View File
@@ -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) {
+5 -2
View File
@@ -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,
+1 -1
View File
@@ -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);