123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- use std::{
- collections::{HashMap, HashSet},
- fmt,
- path::{Path, PathBuf, MAIN_SEPARATOR},
- sync::{Arc, Mutex},
- };
- pub use glob::Pattern;
- use tauri_utils::{
- config::{Config, FsAllowlistScope},
- Env, PackageInfo,
- };
- use uuid::Uuid;
- use crate::api::path::parse as parse_path;
- /// Scope change event.
- #[derive(Debug, Clone)]
- pub enum Event {
- /// A path has been allowed.
- PathAllowed(PathBuf),
- /// A path has been forbidden.
- PathForbidden(PathBuf),
- }
- type EventListener = Box<dyn Fn(&Event) + Send>;
- /// Scope for filesystem access.
- #[derive(Clone)]
- pub struct Scope {
- alllowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
- forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
- event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
- }
- impl fmt::Debug for Scope {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Scope")
- .field(
- "alllowed_patterns",
- &self
- .alllowed_patterns
- .lock()
- .unwrap()
- .iter()
- .map(|p| p.as_str())
- .collect::<Vec<&str>>(),
- )
- .field(
- "forbidden_patterns",
- &self
- .forbidden_patterns
- .lock()
- .unwrap()
- .iter()
- .map(|p| p.as_str())
- .collect::<Vec<&str>>(),
- )
- .finish()
- }
- }
- fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
- list: &mut HashSet<Pattern>,
- pattern: P,
- f: F,
- ) -> crate::Result<()> {
- let path: PathBuf = pattern.as_ref().components().collect();
- list.insert(f(&path.to_string_lossy())?);
- #[cfg(windows)]
- {
- if let Ok(p) = std::fs::canonicalize(&path) {
- list.insert(f(&p.to_string_lossy())?);
- } else {
- list.insert(f(&format!("\\\\?\\{}", path.display()))?);
- }
- }
- Ok(())
- }
- impl Scope {
- /// Creates a new scope from a `FsAllowlistScope` configuration.
- pub(crate) fn for_fs_api(
- config: &Config,
- package_info: &PackageInfo,
- env: &Env,
- scope: &FsAllowlistScope,
- ) -> crate::Result<Self> {
- let mut alllowed_patterns = HashSet::new();
- for path in scope.allowed_paths() {
- if let Ok(path) = parse_path(config, package_info, env, path) {
- push_pattern(&mut alllowed_patterns, path, Pattern::new)?;
- }
- }
- let mut forbidden_patterns = HashSet::new();
- if let Some(forbidden_paths) = scope.forbidden_paths() {
- for path in forbidden_paths {
- if let Ok(path) = parse_path(config, package_info, env, path) {
- push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
- }
- }
- }
- Ok(Self {
- alllowed_patterns: Arc::new(Mutex::new(alllowed_patterns)),
- forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
- event_listeners: Default::default(),
- })
- }
- /// The list of allowed patterns.
- pub fn allowed_patterns(&self) -> HashSet<Pattern> {
- self.alllowed_patterns.lock().unwrap().clone()
- }
- /// The list of forbidden patterns.
- pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
- self.forbidden_patterns.lock().unwrap().clone()
- }
- /// Listen to an event on this scope.
- pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
- let id = Uuid::new_v4();
- self.event_listeners.lock().unwrap().insert(id, Box::new(f));
- id
- }
- fn trigger(&self, event: Event) {
- let listeners = self.event_listeners.lock().unwrap();
- let handlers = listeners.values();
- for listener in handlers {
- listener(&event);
- }
- }
- /// Extend the allowed patterns with the given directory.
- ///
- /// 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.
- pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
- let path = path.as_ref();
- {
- let mut list = self.alllowed_patterns.lock().unwrap();
- // allow the directory to be read
- push_pattern(&mut list, &path, escaped_pattern)?;
- // allow its files and subdirectories to be read
- push_pattern(&mut list, &path, |p| {
- escaped_pattern_with(p, if recursive { "**" } else { "*" })
- })?;
- }
- self.trigger(Event::PathAllowed(path.to_path_buf()));
- Ok(())
- }
- /// 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.
- pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
- let path = path.as_ref();
- push_pattern(
- &mut self.alllowed_patterns.lock().unwrap(),
- &path,
- escaped_pattern,
- )?;
- self.trigger(Event::PathAllowed(path.to_path_buf()));
- Ok(())
- }
- /// Set the given directory path to be forbidden by this scope.
- ///
- /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
- pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
- let path = path.as_ref();
- {
- let mut list = self.forbidden_patterns.lock().unwrap();
- // allow the directory to be read
- push_pattern(&mut list, &path, escaped_pattern)?;
- // allow its files and subdirectories to be read
- push_pattern(&mut list, &path, |p| {
- escaped_pattern_with(p, if recursive { "**" } else { "*" })
- })?;
- }
- self.trigger(Event::PathForbidden(path.to_path_buf()));
- Ok(())
- }
- /// Set the given file path to be forbidden by this scope.
- ///
- /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
- pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
- let path = path.as_ref();
- push_pattern(
- &mut self.forbidden_patterns.lock().unwrap(),
- &path,
- escaped_pattern,
- )?;
- self.trigger(Event::PathForbidden(path.to_path_buf()));
- Ok(())
- }
- /// Determines if the given path is allowed on this scope.
- pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
- let path = path.as_ref();
- let path = if !path.exists() {
- crate::Result::Ok(path.to_path_buf())
- } else {
- std::fs::canonicalize(path).map_err(Into::into)
- };
- if let Ok(path) = path {
- let path: PathBuf = path.components().collect();
- let options = glob::MatchOptions {
- // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
- // see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5
- require_literal_separator: true,
- // dotfiles are not supposed to be exposed by default
- #[cfg(unix)]
- require_literal_leading_dot: true,
- ..Default::default()
- };
- let forbidden = self
- .forbidden_patterns
- .lock()
- .unwrap()
- .iter()
- .any(|p| p.matches_path_with(&path, options));
- if forbidden {
- false
- } else {
- let allowed = self
- .alllowed_patterns
- .lock()
- .unwrap()
- .iter()
- .any(|p| p.matches_path_with(&path, options));
- allowed
- }
- } else {
- false
- }
- }
- }
- fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
- Pattern::new(&glob::Pattern::escape(p))
- }
- fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
- Pattern::new(&format!(
- "{}{}{}",
- glob::Pattern::escape(p),
- MAIN_SEPARATOR,
- append
- ))
- }
- #[cfg(test)]
- mod tests {
- use super::Scope;
- fn new_scope() -> Scope {
- Scope {
- alllowed_patterns: Default::default(),
- forbidden_patterns: Default::default(),
- event_listeners: Default::default(),
- }
- }
- #[test]
- fn path_is_escaped() {
- let scope = new_scope();
- #[cfg(unix)]
- {
- scope.allow_directory("/home/tauri/**", false).unwrap();
- assert!(scope.is_allowed("/home/tauri/**"));
- assert!(scope.is_allowed("/home/tauri/**/file"));
- assert!(!scope.is_allowed("/home/tauri/anyfile"));
- }
- #[cfg(windows)]
- {
- scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
- assert!(scope.is_allowed("C:\\home\\tauri\\**"));
- assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
- }
- let scope = new_scope();
- #[cfg(unix)]
- {
- scope.allow_file("/home/tauri/**").unwrap();
- assert!(scope.is_allowed("/home/tauri/**"));
- assert!(!scope.is_allowed("/home/tauri/**/file"));
- assert!(!scope.is_allowed("/home/tauri/anyfile"));
- }
- #[cfg(windows)]
- {
- scope.allow_file("C:\\home\\tauri\\**").unwrap();
- assert!(scope.is_allowed("C:\\home\\tauri\\**"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
- }
- let scope = new_scope();
- #[cfg(unix)]
- {
- scope.allow_directory("/home/tauri", true).unwrap();
- scope.forbid_directory("/home/tauri/**", false).unwrap();
- assert!(!scope.is_allowed("/home/tauri/**"));
- assert!(!scope.is_allowed("/home/tauri/**/file"));
- assert!(scope.is_allowed("/home/tauri/**/inner/file"));
- assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
- assert!(scope.is_allowed("/home/tauri/anyfile"));
- }
- #[cfg(windows)]
- {
- scope.allow_directory("C:\\home\\tauri", true).unwrap();
- scope
- .forbid_directory("C:\\home\\tauri\\**", false)
- .unwrap();
- assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
- assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
- assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
- assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
- }
- let scope = new_scope();
- #[cfg(unix)]
- {
- scope.allow_directory("/home/tauri", true).unwrap();
- scope.forbid_file("/home/tauri/**").unwrap();
- assert!(!scope.is_allowed("/home/tauri/**"));
- assert!(scope.is_allowed("/home/tauri/**/file"));
- assert!(scope.is_allowed("/home/tauri/**/inner/file"));
- assert!(scope.is_allowed("/home/tauri/anyfile"));
- }
- #[cfg(windows)]
- {
- scope.allow_directory("C:\\home\\tauri", true).unwrap();
- scope.forbid_file("C:\\home\\tauri\\**").unwrap();
- assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
- assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
- assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
- assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
- }
- let scope = new_scope();
- #[cfg(unix)]
- {
- scope.allow_directory("/home/tauri", false).unwrap();
- assert!(scope.is_allowed("/home/tauri/**"));
- assert!(!scope.is_allowed("/home/tauri/**/file"));
- assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
- assert!(scope.is_allowed("/home/tauri/anyfile"));
- }
- #[cfg(windows)]
- {
- scope.allow_directory("C:\\home\\tauri", false).unwrap();
- assert!(scope.is_allowed("C:\\home\\tauri\\**"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
- assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
- assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
- }
- }
- }
|