Browse / github.com/storytold/soundcraft / patch
Fix clip group import rejecting audio names containing ".."
Fix clip group import rejecting audio names containing ".." Found while testing `crates/engine/src/clip_group_file.rs:101` (no existing issue): Clip group import rejects its own export when a source name contains "..". ## Cause ## Cause The import path-traversal guard used `rel.contains("..")`, which also matches ordinary file names with two consecutive dots. Export keeps dots in file names, so a source named "Wait... what" produced a group file that import then rejected. ## Fix ## Fix The guard now checks the path's components for `ParentDir` instead of searching for the substring "..". The existing `starts_with(dir)` check stays, so absolute paths
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #1020, open)
- Base
aa74bffac1aebfeedee0a3b60729ad8ceefc2195- Size
- +29 −1 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
f7fec68d30cc281ee830a2fa
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/clip_group_file.rs
@@ -89,7 +89,7 @@ fn decode_group_audio( let (mut decoded, mut index, mut file_of) = (Vec::new(), BTreeMap::new(), BTreeMap::new()); for (old, rel) in audio_entries { let full = dir.join(rel);- if !full.starts_with(dir) || rel.contains("..") {+ if !full.starts_with(dir) || Path::new(rel).components().any(|c| matches!(c, std::path::Component::ParentDir)) { return Err(EngineError::BadParams(cmd_id.into(), format!("audio path escapes the group folder: {rel}"))); } let key = crate::io::file_key(&full);@@ -231,6 +231,34 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test]+ fn import_accepts_audio_names_with_consecutive_dots_but_not_parent_dirs() {+ let dir = std::env::temp_dir().join(format!("sc-grp-dots-{}", std::process::id()));+ let _ = std::fs::create_dir_all(&dir);+ let path = dir.join("drums.scgrp").to_string_lossy().to_string();+ let mut e = crate::demo::demo_engine();+ let kick = e.session().tracks.iter().find(|t| t.name == "Kick").map(|t| t.id).unwrap();+ let ids: Vec<_> = e.session().track(kick).unwrap().clips().iter().take(1).map(|c| c.id.0).collect();+ e.execute("file.export_clip_groups", &json!({"path": path, "clips": ids})).unwrap();+ let mut file: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();+ for (i, rel) in file["audio"].as_object_mut().unwrap().values_mut().enumerate() {+ let old = dir.join(rel.as_str().unwrap());+ let new = format!("Wait... what_{i}.wav");+ std::fs::rename(&old, dir.join(&new)).unwrap();+ *rel = json!(new);+ }+ std::fs::write(&path, file.to_string()).unwrap();+ let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));+ assert!(r.is_ok(), "{r:?}");+ for rel in file["audio"].as_object_mut().unwrap().values_mut() {+ *rel = json!("../escape.wav");+ }+ std::fs::write(&path, file.to_string()).unwrap();+ let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));+ assert!(matches!(r, Err(crate::EngineError::BadParams(..))), "{r:?}");+ let _ = std::fs::remove_dir_all(&dir);+ }+ #[test] fn positions_in_a_group_file_are_kept_within_reach() { let dir = std::env::temp_dir().join(format!("sc-grp-far-{}", std::process::id()));