From f323eb8f56e55fc23e0fff0d1fe0a55e32455262 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Tue, 19 Nov 2024 18:58:37 +0100 Subject: [PATCH] i am making it worse --- plugins/fs/Cargo.toml | 2 +- plugins/fs/build.rs | 2 +- plugins/fs/src/commands.rs | 87 +++++++++++++++++----- plugins/fs/src/entryraw.rs | 14 ++++ plugins/fs/src/lib.rs | 19 +++-- plugins/fs/src/scope.rs | 146 ++++++++++++++----------------------- 6 files changed, 151 insertions(+), 119 deletions(-) create mode 100644 plugins/fs/src/entryraw.rs diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 9763f193..07610a1f 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -14,7 +14,7 @@ rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"] [package.metadata.platforms.support] -windows = { level = "full", notes = "No write access to `$RESOURCES` folder with MSI installer and NSIS installers in `perMachine` or `both` mode" } +windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write acces in `$RESOURCES` folder" } linux = { level = "full", notes = "No write access to `$RESOURCES` folder" } macos = { level = "full", notes = "No write access to `$RESOURCES` folder" } android = { level = "partial", notes = "Access is restricted to Application folder by default" } diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs index cb9d00da..66a12552 100644 --- a/plugins/fs/build.rs +++ b/plugins/fs/build.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, }; -#[path = "src/scope.rs"] +#[path = "src/entryraw.rs"] #[allow(dead_code)] mod scope; diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index 3b5cc44e..efa8891f 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -16,7 +16,7 @@ use std::{ borrow::Cow, fs::File, io::{BufReader, Lines, Read, Write}, - path::PathBuf, + path::{Path, PathBuf}, str::FromStr, sync::Mutex, time::{SystemTime, UNIX_EPOCH}, @@ -1002,37 +1002,88 @@ pub fn resolve_path( let scope = tauri::scope::fs::Scope::new( webview, &FsScope::Scope { - allow: webview - .fs_scope() - .allowed - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.allows().iter().filter_map(|e| e.path.clone())) + allow: global_scope + .allows() + .iter() + .filter_map(|e| e.path.clone()) .chain(command_scope.allows().iter().filter_map(|e| e.path.clone())) .collect(), - deny: webview - .fs_scope() - .denied - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.denies().iter().filter_map(|e| e.path.clone())) + deny: global_scope + .denies() + .iter() + .filter_map(|e| e.path.clone()) .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) .collect(), require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot, }, )?; - if scope.is_allowed(&path) { + let fs_scope = webview.fs_scope(); + + let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix)); + + if fs_scope + .scope + .as_ref() + .map(|s| is_forbidden(s, &path, require_literal_leading_dot)) + .unwrap_or(false) + || is_forbidden(&scope, &path, require_literal_leading_dot) + { + return Err(CommandError::Plugin(Error::PathForbidden(path))); + } + + if fs_scope + .scope + .as_ref() + .map(|s| s.is_allowed(&path)) + .unwrap_or(false) + || scope.is_allowed(&path) + { Ok(path) } else { Err(CommandError::Plugin(Error::PathForbidden(path))) } } +fn is_forbidden>( + scope: &tauri::fs::Scope, + path: P, + require_literal_leading_dot: bool, +) -> bool { + let path = path.as_ref(); + let path = if path.is_symlink() { + match std::fs::read_link(path) { + Ok(p) => p, + Err(_) => return false, + } + } else { + path.to_path_buf() + }; + let path = if !path.exists() { + crate::Result::Ok(path) + } else { + std::fs::canonicalize(path).map_err(Into::into) + }; + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + scope.forbidden_patterns().iter().any(|p| { + p.matches_path_with( + &path, + glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + require_literal_leading_dot, + ..Default::default() + }, + ) + }) + } else { + false + } +} + struct StdFileResource(Mutex); impl StdFileResource { diff --git a/plugins/fs/src/entryraw.rs b/plugins/fs/src/entryraw.rs new file mode 100644 index 00000000..36d195c4 --- /dev/null +++ b/plugins/fs/src/entryraw.rs @@ -0,0 +1,14 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum EntryRaw { + Value(PathBuf), + Object { path: PathBuf }, +} diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index b48b24dc..126c0389 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -15,7 +15,7 @@ use serde::Deserialize; use tauri::{ ipc::ScopeObject, plugin::{Builder as PluginBuilder, TauriPlugin}, - utils::acl::Value, + utils::{acl::Value, config::FsScope}, AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, }; @@ -23,6 +23,7 @@ mod commands; mod config; #[cfg(not(target_os = "android"))] mod desktop; +mod entryraw; mod error; mod file_path; #[cfg(target_os = "android")] @@ -352,8 +353,8 @@ impl ScopeObject for scope::Entry { raw: Value, ) -> std::result::Result { let path = serde_json::from_value(raw.into()).map(|raw| match raw { - scope::EntryRaw::Value(path) => path, - scope::EntryRaw::Object { path } => path, + entryraw::EntryRaw::Value(path) => path, + entryraw::EntryRaw::Object { path } => path, })?; match app.path().parse(path) { @@ -419,11 +420,13 @@ pub fn init() -> TauriPlugin> { watcher::unwatch ]) .setup(|app, api| { - let mut scope = Scope::default(); - scope.require_literal_leading_dot = api - .config() - .as_ref() - .and_then(|c| c.require_literal_leading_dot); + let scope = Scope { + require_literal_leading_dot: api + .config() + .as_ref() + .and_then(|c| c.require_literal_leading_dot), + scope: Some(tauri::fs::Scope::new(app, &FsScope::default())?), + }; #[cfg(target_os = "android")] { diff --git a/plugins/fs/src/scope.rs b/plugins/fs/src/scope.rs index ff3e046a..44534d1d 100644 --- a/plugins/fs/src/scope.rs +++ b/plugins/fs/src/scope.rs @@ -3,30 +3,16 @@ // SPDX-License-Identifier: MIT use std::{ - collections::HashMap, + collections::HashSet, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicU32, Ordering}, - Mutex, - }, }; -use serde::Deserialize; - -#[derive(Deserialize)] -#[serde(untagged)] -pub(crate) enum EntryRaw { - Value(PathBuf), - Object { path: PathBuf }, -} - #[derive(Debug)] pub struct Entry { pub path: Option, } pub type EventId = u32; -type EventListener = Box; /// Scope change event. #[derive(Debug, Clone)] @@ -39,10 +25,8 @@ pub enum Event { #[derive(Default)] pub struct Scope { - pub(crate) allowed: Mutex>, - pub(crate) denied: Mutex>, - event_listeners: Mutex>, - next_event_id: AtomicU32, + // TODO: Remove Option in v2, just used to keep Default + pub(crate) scope: Option, pub(crate) require_literal_leading_dot: Option, } @@ -51,113 +35,93 @@ impl Scope { /// /// After this function has been called, the frontend will be able to use the Tauri API to read /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. + // TODO: Return Result pub fn allow_directory>(&self, path: P, recursive: bool) { - let path = path.as_ref(); - - { - let mut allowed = self.allowed.lock().unwrap(); - let p = path.to_string_lossy(); - allowed.push(escape(&p)); - allowed.push(PathBuf::from(if recursive { "**" } else { "*" })); + if let Some(scope) = &self.scope { + let _ = scope.allow_directory(path, recursive); } - - self.emit(Event::PathAllowed(path.to_path_buf())); } /// Extend the allowed patterns with the given file path. /// /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. + // TODO: Return Result pub fn allow_file>(&self, path: P) { - let path = path.as_ref(); - - self.allowed - .lock() - .unwrap() - .push(escape(&path.to_string_lossy())); - - self.emit(Event::PathAllowed(path.to_path_buf())); + if let Some(scope) = &self.scope { + let _ = scope.allow_file(path); + } } /// Set the given directory path to be forbidden by this scope. /// /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + // TODO: Return Result pub fn forbid_directory>(&self, path: P, recursive: bool) { - let path = path.as_ref(); - - { - let mut denied = self.denied.lock().unwrap(); - let p = path.to_string_lossy(); - denied.push(escape(&p)); - denied.push(PathBuf::from(if recursive { "**" } else { "*" })); + if let Some(scope) = &self.scope { + let _ = scope.forbid_directory(path, recursive); } - - self.emit(Event::PathForbidden(path.to_path_buf())); } /// Set the given file path to be forbidden by this scope. /// /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + // TODO: Return Result pub fn forbid_file>(&self, path: P) { - let path = path.as_ref(); - - self.denied - .lock() - .unwrap() - .push(escape(&path.to_string_lossy())); - - self.emit(Event::PathForbidden(path.to_path_buf())); + if let Some(scope) = &self.scope { + let _ = scope.forbid_file(path); + } } /// List of allowed paths. + #[deprecated(since = "2.1.0", note = "use `allowed_patterns` instead")] pub fn allowed(&self) -> Vec { - self.allowed.lock().unwrap().clone() + self.scope + .as_ref() + .map(|s| s.allowed_patterns().clone()) + .unwrap_or_default() + .iter() + .map(|p| PathBuf::from(p.as_str())) + .collect() } - /// List of forbidden paths. - pub fn forbidden(&self) -> Vec { - self.denied.lock().unwrap().clone() + /// List of allowed patterns. Note that this does not include paths defined in capabilites. + pub fn allowed_patterns(&self) -> HashSet { + self.scope + .as_ref() + .map(|s| s.allowed_patterns().clone()) + .unwrap_or_default() } - fn next_event_id(&self) -> u32 { - self.next_event_id.fetch_add(1, Ordering::Relaxed) + /// List of forbidden paths. + #[deprecated(since = "2.1.0", note = "use `forbidden_patterns` instead")] + pub fn forbidden(&self) -> Vec { + self.scope + .as_ref() + .map(|s| s.forbidden_patterns().clone()) + .unwrap_or_default() + .iter() + .map(|p| PathBuf::from(p.as_str())) + .collect() } - fn emit(&self, event: Event) { - let listeners = self.event_listeners.lock().unwrap(); - let handlers = listeners.values(); - for listener in handlers { - listener(&event); - } + /// List of forbidden patterns. Note that this does not include paths defined in capabilites. + pub fn forbidden_patterns(&self) -> HashSet { + self.scope + .as_ref() + .map(|s| s.forbidden_patterns()) + .unwrap_or_default() } /// Listen to an event on this scope. + /// Silently fails and returns `0` until v3 if `Scope` was constructed manually instead of getting it via `app.fs_scope()`. pub fn listen(&self, f: F) -> EventId { - let id = self.next_event_id(); - self.event_listeners.lock().unwrap().insert(id, Box::new(f)); - id - } -} - -// taken from https://github.com/rust-lang/glob/blob/master/src/lib.rs#L717C5-L737C6 -/// Escape metacharacters within the given string by surrounding them in -/// brackets. The resulting string will, when compiled into a `Pattern`, -/// match the input string and nothing else. -pub fn escape(s: &str) -> PathBuf { - let mut escaped = String::new(); - for c in s.chars() { - match c { - // note that ! does not need escaping because it is only special - // inside brackets - /* disabled to not break paths '?' | */ - '*' | '[' | ']' => { - escaped.push('['); - escaped.push(c); - escaped.push(']'); - } - c => { - escaped.push(c); - } + if let Some(scope) = &self.scope { + scope.listen(move |e| match e { + tauri::fs::Event::PathAllowed(p) => f(&Event::PathAllowed(p.to_owned())), + tauri::fs::Event::PathForbidden(p) => f(&Event::PathForbidden(p.to_owned())), + }) + } else { + 0 } } - PathBuf::from(escaped) }