From 81e4e262909c4cebff62dbc5fe0e9ffbf525d02e Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Mon, 17 Aug 2026 20:14:34 +0200 Subject: [PATCH] fix: restrict asset protocol to vaults --- src-tauri/Cargo.toml | 3 ++ src-tauri/src/asset_scope.rs | 68 ++++++++++++++++++++++++++++++++++++ src-tauri/src/commands.rs | 2 ++ src-tauri/src/lib.rs | 15 ++++++++ src-tauri/tauri.conf.json | 6 +++- 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/asset_scope.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 425613b..cc61f78 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,6 +14,9 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } +[dev-dependencies] +tauri = { version = "2", features = ["test"] } + [dependencies] tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] } tauri-plugin-log = "2" diff --git a/src-tauri/src/asset_scope.rs b/src-tauri/src/asset_scope.rs new file mode 100644 index 0000000..95b1521 --- /dev/null +++ b/src-tauri/src/asset_scope.rs @@ -0,0 +1,68 @@ +use std::path::Path; +use tauri::{AppHandle, Manager, Runtime}; + +/// Grants asset-protocol access only to a vault selected by the user. +/// `.helixnotes` is added explicitly because Unix glob matching excludes hidden paths. +pub fn allow_vault_assets(app: &AppHandle, vault_path: &Path) -> Result<(), String> { + // Keep the canonical native path: Tauri's scope normalizes Unix paths plus + // Windows drive, verbatim, and UNC prefixes when matching asset requests. + let vault = std::fs::canonicalize(vault_path).map_err(|error| { + format!( + "Failed to resolve vault asset path '{}': {error}", + vault_path.display() + ) + })?; + let scope = app.asset_protocol_scope(); + scope + .allow_directory(&vault, true) + .map_err(|error| error.to_string())?; + scope + .allow_directory(vault.join(".helixnotes"), true) + .map_err(|error| error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn test_directory(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("helixnotes-{name}-{}", uuid::Uuid::new_v4())) + } + + #[test] + fn static_asset_scope_does_not_expose_the_filesystem() { + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + let scope = &config["app"]["security"]["assetProtocol"]["scope"]; + + assert_eq!(scope["allow"], serde_json::json!([])); + assert_eq!(scope["deny"], serde_json::json!([])); + assert_eq!(scope["requireLiteralLeadingDot"], true); + } + + #[test] + fn runtime_scope_allows_only_the_selected_vault() { + let root = test_directory("asset-scope"); + let vault = root.join("Vault [Cross Platform]"); + let attachments = vault.join(".helixnotes").join("attachments"); + fs::create_dir_all(&attachments).unwrap(); + let note_image = vault.join("images").join("note image.png"); + fs::create_dir_all(note_image.parent().unwrap()).unwrap(); + fs::write(¬e_image, b"image").unwrap(); + let attachment = attachments.join("attachment.png"); + fs::write(&attachment, b"attachment").unwrap(); + let outside = root.join("outside.png"); + fs::write(&outside, b"outside").unwrap(); + + let app = tauri::test::mock_app(); + allow_vault_assets(app.handle(), &vault).unwrap(); + let scope = app.asset_protocol_scope(); + + assert!(scope.is_allowed(¬e_image)); + assert!(scope.is_allowed(&attachment)); + assert!(!scope.is_allowed(&outside)); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2c9efc2..8ed4dd1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,3 +1,4 @@ +use crate::asset_scope; use crate::search::SearchIndex; use crate::state::AppState; use crate::types::*; @@ -94,6 +95,7 @@ fn open_vault_path( search.rebuild(&path)?; let new_watcher = watcher::start_watcher(app.clone(), path.clone())?; + asset_scope::allow_vault_assets(&app, Path::new(&path))?; // Update config. External vaults use the bookmark as their stable identity; // the resolved path is refreshed whenever the vault opens. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 41ccebf..f4032e0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod ai; +mod asset_scope; mod backup; mod commands; mod history; @@ -93,6 +94,20 @@ pub fn run() { }); } + let active_vault = app + .state::() + .config + .lock() + .ok() + .and_then(|config| config.active_vault.clone()); + if let Some(vault_path) = active_vault { + if let Err(error) = + asset_scope::allow_vault_assets(app.handle(), std::path::Path::new(&vault_path)) + { + log::warn!("Failed to restore the vault asset scope: {error}"); + } + } + #[cfg(desktop)] if show_tray { setup_tray(app)?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dff8cf2..8e652d3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -27,7 +27,11 @@ "csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost imgproxy: http://imgproxy.localhost https: blob: data:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self' asset: http://asset.localhost", "assetProtocol": { "enable": true, - "scope": ["**/*", "/**", "**/.helixnotes/**"] + "scope": { + "allow": [], + "deny": [], + "requireLiteralLeadingDot": true + } } } },