diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 37b96c4..d598447 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -113,73 +113,6 @@ fn atomic_write(target: &Path, bytes: &[u8]) -> std::io::Result<()> { Ok(()) } -fn safe_path_component<'a>(value: &'a str, label: &str) -> Result<&'a str, String> { - if value.is_empty() - || value == "." - || value == ".." - || value.contains(['/', '\\']) - || Path::new(value).is_absolute() - { - return Err(format!("Invalid {}", label)); - } - Ok(value) -} - -fn resolve_image_directory(parent_dir: &str, image_directory: &str) -> Result<(PathBuf, PathBuf), String> { - let root = Path::new(parent_dir) - .canonicalize() - .map_err(|e| format!("Invalid image parent directory: {}", e))?; - let requested_dir = if image_directory.is_empty() { - root.clone() - } else { - root.join(safe_path_component(image_directory, "image directory")?) - }; - - fs::create_dir_all(&requested_dir).map_err(|e| e.to_string())?; - let image_dir = requested_dir.canonicalize().map_err(|e| e.to_string())?; - if !image_dir.starts_with(&root) { - return Err("Image directory must remain inside the document directory".to_string()); - } - Ok((root, image_dir)) -} - -fn ensure_path_within_root(root: &Path, path: &Path) -> Result<(), String> { - let resolved = match fs::symlink_metadata(path) { - Ok(metadata) if metadata.file_type().is_symlink() => path.canonicalize().map_err(|e| e.to_string())?, - _ => path.to_path_buf(), - }; - if resolved.starts_with(root) { - Ok(()) - } else { - Err("Image path must remain inside the document directory".to_string()) - } -} - -const MAX_VSIX_DOWNLOAD_BYTES: usize = 20 * 1024 * 1024; -const MAX_VSIX_ENTRIES: usize = 10_000; -const MAX_VSIX_UNCOMPRESSED_BYTES: u64 = 100 * 1024 * 1024; -const MAX_THEME_JSON_BYTES: u64 = 2 * 1024 * 1024; - -fn validate_vsix_archive_limits( - archive: &mut zip::ZipArchive, -) -> Result<(), String> { - if archive.len() > MAX_VSIX_ENTRIES { - return Err("VSIX contains too many files".to_string()); - } - - let mut total_size = 0_u64; - for index in 0..archive.len() { - let file = archive.by_index(index).map_err(|e| e.to_string())?; - total_size = total_size - .checked_add(file.size()) - .ok_or("VSIX uncompressed size overflow")?; - if total_size > MAX_VSIX_UNCOMPRESSED_BYTES { - return Err("VSIX expands beyond the allowed size".to_string()); - } - } - Ok(()) -} - #[cfg(test)] mod tests { use super::*; @@ -266,35 +199,6 @@ mod tests { assert!(out.contains("`^[not a footnote]`"), "got: {out}"); assert!(out.contains("[jump](#real)"), "got: {out}"); } - - #[test] - fn path_components_reject_traversal_separators_and_absolute_paths() { - for invalid in ["", ".", "..", "../theme", "folder/theme", "folder\\theme", "/tmp/theme"] { - assert!(safe_path_component(invalid, "test").is_err(), "{invalid}"); - } - assert_eq!(safe_path_component("SynthWave '84", "test").unwrap(), "SynthWave '84"); - } - - #[cfg(unix)] - #[test] - fn image_directory_rejects_symlink_escape() { - use std::os::unix::fs::symlink; - - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("markpad-path-root-{nonce}")); - let outside = std::env::temp_dir().join(format!("markpad-path-outside-{nonce}")); - fs::create_dir_all(&root).unwrap(); - fs::create_dir_all(&outside).unwrap(); - symlink(&outside, root.join("images")).unwrap(); - - assert!(resolve_image_directory(root.to_str().unwrap(), "images").is_err()); - - fs::remove_dir_all(root).unwrap(); - fs::remove_dir_all(outside).unwrap(); - } } struct WatcherState { @@ -876,7 +780,7 @@ fn save_theme(app: AppHandle, theme: String) -> Result<(), String> { let config_dir = app.path().app_config_dir().map_err(|e| e.to_string())?; fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; let theme_path = config_dir.join("theme.txt"); - atomic_write(&theme_path, theme.as_bytes()).map_err(|e| e.to_string()) + fs::write(theme_path, theme).map_err(|e| e.to_string()) } #[tauri::command] @@ -932,30 +836,14 @@ async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result MAX_VSIX_DOWNLOAD_BYTES as u64) { - return Err("VSIX download exceeds the allowed size".to_string()); - } - let mut bytes = Vec::new(); - while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? { - if bytes.len() + chunk.len() > MAX_VSIX_DOWNLOAD_BYTES { - return Err("VSIX download exceeds the allowed size".to_string()); - } - bytes.extend_from_slice(&chunk); - } + let response = reqwest::get(&vsix_url).await.map_err(|e| e.to_string())?; + let bytes = response.bytes().await.map_err(|e| e.to_string())?; - let reader = Cursor::new(bytes); + let reader = Cursor::new(bytes.as_ref()); let mut archive = zip::ZipArchive::new(reader).map_err(|e| e.to_string())?; - validate_vsix_archive_limits(&mut archive)?; let mut package_json_data = String::new(); if let Ok(mut file) = archive.by_name("extension/package.json") { - if file.size() > MAX_THEME_JSON_BYTES { - return Err("VSIX package manifest exceeds the allowed size".to_string()); - } file.read_to_string(&mut package_json_data) .map_err(|e| e.to_string())?; } else { @@ -1004,9 +892,6 @@ async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result MAX_THEME_JSON_BYTES { - return Err("VSIX theme file exceeds the allowed size".to_string()); - } let mut theme_json = String::new(); theme_file .read_to_string(&mut theme_json) @@ -1021,11 +906,10 @@ async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result Result, String> { #[tauri::command] fn read_vscode_theme(app: AppHandle, name: String) -> Result { let config_dir = app.path().app_config_dir().map_err(|e| e.to_string())?; - let name = safe_path_component(&name, "theme name")?; let theme_file_path = config_dir.join("themes").join(format!("{}.json", name)); fs::read_to_string(theme_file_path).map_err(|e| e.to_string()) } @@ -1061,7 +944,6 @@ fn read_vscode_theme(app: AppHandle, name: String) -> Result { #[tauri::command] fn delete_vscode_theme(app: AppHandle, name: String) -> Result<(), String> { let config_dir = app.path().app_config_dir().map_err(|e| e.to_string())?; - let name = safe_path_component(&name, "theme name")?; let theme_file_path = config_dir.join("themes").join(format!("{}.json", name)); fs::remove_file(theme_file_path).map_err(|e| e.to_string()) } @@ -1218,10 +1100,12 @@ fn clipboard_read_image(macos_image_scaling: bool) -> Result { #[tauri::command] fn save_image(parent_dir: String, filename: String, base64_data: String, image_directory: String) -> Result { - let filename = safe_path_component(&filename, "image filename")?; - let (root, img_dir) = resolve_image_directory(&parent_dir, &image_directory)?; - let file_path = img_dir.join(filename); - ensure_path_within_root(&root, &file_path)?; + let img_dir = Path::new(&parent_dir).join(&image_directory); + if !img_dir.exists() { + fs::create_dir_all(&img_dir).map_err(|e| e.to_string())?; + } + + let file_path = img_dir.join(&filename); // remove potential data:image/png;base64, prefix let b64 = if let Some(pos) = base64_data.find("base64,") { @@ -1235,10 +1119,10 @@ fn save_image(parent_dir: String, filename: String, base64_data: String, image_d .decode(b64) .map_err(|e: base64::DecodeError| e.to_string())?; - atomic_write(&file_path, &bytes).map_err(|e| e.to_string())?; + fs::write(&file_path, bytes).map_err(|e| e.to_string())?; let rel_path = if image_directory.is_empty() { - filename.to_string() + filename } else { format!("{}/{}", image_directory, filename) }; @@ -1248,7 +1132,10 @@ fn save_image(parent_dir: String, filename: String, base64_data: String, image_d #[tauri::command] fn copy_file_to_img(src_path: String, parent_dir: String, image_directory: String) -> Result { - let (root, img_dir) = resolve_image_directory(&parent_dir, &image_directory)?; + let img_dir = Path::new(&parent_dir).join(&image_directory); + if !img_dir.exists() { + fs::create_dir_all(&img_dir).map_err(|e| e.to_string())?; + } let src = Path::new(&src_path); if !src.exists() { @@ -1270,7 +1157,6 @@ fn copy_file_to_img(src_path: String, parent_dir: String, image_directory: Strin } let final_dest = img_dir.join(&dest_name); - ensure_path_within_root(&root, &final_dest)?; fs::copy(src, &final_dest).map_err(|e| e.to_string())?; let rel_path = if image_directory.is_empty() {