From 3572c7a6b9d6b6c6d514ed7786a878ea30b9120f Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 22 Jun 2026 13:34:51 -0400 Subject: [PATCH] fix(security): validate book id paths load/save/delete_book built paths from the raw id, and ids round-trip from file JSON, so a crafted id could escape the library directory. Route all three through a book_path helper that rejects anything but [A-Za-z0-9_-]. Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk --- src-tauri/src/library.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 236107f..1999390 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -29,6 +29,13 @@ fn library_dir(app: &tauri::AppHandle) -> Result { Ok(dir) } +fn book_path(app: &tauri::AppHandle, id: &str) -> Result { + if id.is_empty() || !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { + return Err("invalid book id".to_string()); + } + Ok(library_dir(app)?.join(format!("{id}.margin"))) +} + #[tauri::command] pub fn list_books(app: tauri::AppHandle) -> Result, String> { let dir = library_dir(&app)?; @@ -84,19 +91,19 @@ pub fn list_books(app: tauri::AppHandle) -> Result, String> { #[tauri::command] pub fn load_book(app: tauri::AppHandle, id: String) -> Result { - let path = library_dir(&app)?.join(format!("{id}.margin")); + let path = book_path(&app, &id)?; fs::read_to_string(&path).map_err(|e| e.to_string()) } #[tauri::command] pub fn save_book(app: tauri::AppHandle, id: String, contents: String) -> Result<(), String> { - let path = library_dir(&app)?.join(format!("{id}.margin")); + let path = book_path(&app, &id)?; crate::project::atomic_write(&path, contents.as_bytes(), true) } #[tauri::command] pub fn delete_book(app: tauri::AppHandle, id: String) -> Result<(), String> { - let path = library_dir(&app)?.join(format!("{id}.margin")); + let path = book_path(&app, &id)?; if path.exists() { fs::remove_file(&path).map_err(|e| e.to_string())?; }