From 144f4f94f8548255e2531eb4042148e2f4850cb9 Mon Sep 17 00:00:00 2001 From: anupamme Date: Sat, 5 Sep 2026 21:19:59 +0000 Subject: [PATCH 1/2] fix: V-002 security vulnerability Automated security fix generated by OrbisAI Security --- src-tauri/src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1d7860c61..39d56eed8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -82,7 +82,16 @@ async fn render_video_ffmpeg( ).map_err(|e| e.to_string())?; } - let output_path = temp_dir.join(format!("{}.mp4", name)); + // Sanitize the user-supplied output name to prevent path traversal / injection + // via special characters (e.g. "../", "/", null bytes) ending up in file paths + // or command arguments. + let safe_name: String = name + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') + .collect(); + let safe_name = if safe_name.is_empty() { "output".to_string() } else { safe_name }; + + let output_path = temp_dir.join(format!("{}.mp4", safe_name)); let mut cmd = Command::new("ffmpeg"); cmd.current_dir(&temp_dir) From dabcdc469c461d5ad134b3003eb3867f1c291513 Mon Sep 17 00:00:00 2001 From: Anupam Mediratta Date: Mon, 7 Sep 2026 07:34:01 +0530 Subject: [PATCH 2/2] fix: reject invalid output names instead of stripping (V-002 follow-up) Narrow the V-002 fix to the actual reachable issue: path traversal via the name parameter feeding output_path in render_video_ffmpeg (CWE-22). Replace the character-stripping sanitizer with an explicit allow-list validator that rejects invalid names outright, so inputs like "../../foo" fail loudly instead of being silently rewritten to "foo". Adds unit tests for the validator. --- src-tauri/src/main.rs | 60 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 39d56eed8..6c472f1f2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -51,6 +51,26 @@ struct Frame { data: Vec, } +/// Validates a user-supplied output base name (no extension) for use in a +/// filesystem path. Rejects (rather than silently rewriting) any input that +/// is empty or contains characters outside `[A-Za-z0-9_-]`, so inputs like +/// "../../etc/passwd" are refused outright instead of being transformed +/// into something unexpectedly different (e.g. "../../foo" -> "foo"). +/// Closes CWE-22 (path traversal) for the `name` parameter that feeds +/// `output_path` in `render_video_ffmpeg`. +fn validate_output_name(name: &str) -> Result<&str, String> { + if name.is_empty() { + return Err("output name must not be empty".to_string()); + } + if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { + return Err(format!( + "invalid output name {:?}: only ASCII letters, digits, '-' and '_' are allowed", + name + )); + } + Ok(name) +} + #[tauri::command] async fn render_video_ffmpeg( frames: Vec, @@ -82,14 +102,10 @@ async fn render_video_ffmpeg( ).map_err(|e| e.to_string())?; } - // Sanitize the user-supplied output name to prevent path traversal / injection - // via special characters (e.g. "../", "/", null bytes) ending up in file paths - // or command arguments. - let safe_name: String = name - .chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') - .collect(); - let safe_name = if safe_name.is_empty() { "output".to_string() } else { safe_name }; + // Reject (rather than silently rewrite) an output name that could cause + // path traversal or otherwise place the rendered file outside the + // intended temp directory. See `validate_output_name` for the allow-list. + let safe_name = validate_output_name(&name)?; let output_path = temp_dir.join(format!("{}.mp4", safe_name)); @@ -242,3 +258,31 @@ fn handle_file_open(app_handle: &AppHandle, path: String) { pending.0.lock().unwrap().insert(label, path); } } + +#[cfg(test)] +mod tests { + use super::validate_output_name; + + #[test] + fn accepts_valid_names_unchanged() { + for valid in ["output", "my-video_01", "AbC123", "a", "___", "---"] { + assert_eq!(validate_output_name(valid), Ok(valid)); + } + } + + #[test] + fn rejects_empty_name() { + assert!(validate_output_name("").is_err()); + } + + #[test] + fn rejects_path_traversal_and_separators() { + let bad_inputs = [ + "../secret", "../../etc/passwd", "/etc/passwd", "a/../../b", + "a\\..\\b", "..\\..\\windows", "foo/bar", "foo\\bar", "..", "foo..bar", + ]; + for input in bad_inputs { + assert!(validate_output_name(input).is_err(), "expected {:?} to be rejected", input); + } + } +}