fix: restrict asset protocol to vaults

This commit is contained in:
Yuri Karamian
2026-08-17 20:14:34 +02:00
parent 111d819f92
commit 81e4e26290
5 changed files with 93 additions and 1 deletions
+3
View File
@@ -14,6 +14,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dev-dependencies]
tauri = { version = "2", features = ["test"] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] } tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
+68
View File
@@ -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<R: Runtime>(app: &AppHandle<R>, 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(&note_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(&note_image));
assert!(scope.is_allowed(&attachment));
assert!(!scope.is_allowed(&outside));
fs::remove_dir_all(root).unwrap();
}
}
+2
View File
@@ -1,3 +1,4 @@
use crate::asset_scope;
use crate::search::SearchIndex; use crate::search::SearchIndex;
use crate::state::AppState; use crate::state::AppState;
use crate::types::*; use crate::types::*;
@@ -94,6 +95,7 @@ fn open_vault_path(
search.rebuild(&path)?; search.rebuild(&path)?;
let new_watcher = watcher::start_watcher(app.clone(), path.clone())?; 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; // Update config. External vaults use the bookmark as their stable identity;
// the resolved path is refreshed whenever the vault opens. // the resolved path is refreshed whenever the vault opens.
+15
View File
@@ -1,4 +1,5 @@
mod ai; mod ai;
mod asset_scope;
mod backup; mod backup;
mod commands; mod commands;
mod history; mod history;
@@ -93,6 +94,20 @@ pub fn run() {
}); });
} }
let active_vault = app
.state::<AppState>()
.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)] #[cfg(desktop)]
if show_tray { if show_tray {
setup_tray(app)?; setup_tray(app)?;
+5 -1
View File
@@ -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", "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": { "assetProtocol": {
"enable": true, "enable": true,
"scope": ["**/*", "/**", "**/.helixnotes/**"] "scope": {
"allow": [],
"deny": [],
"requireLiteralLeadingDot": true
}
} }
} }
}, },