Skip to main content

tauri/scope/
fs.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::{
6  collections::{HashMap, HashSet},
7  fmt,
8  path::{Path, PathBuf, MAIN_SEPARATOR},
9  sync::{
10    atomic::{AtomicU32, Ordering},
11    Arc, Mutex,
12  },
13};
14
15use tauri_utils::config::FsScope;
16
17use crate::ScopeEventId;
18
19pub use glob::Pattern;
20
21/// Scope change event.
22#[derive(Debug, Clone)]
23pub enum Event {
24  /// A path has been allowed.
25  PathAllowed(PathBuf),
26  /// A path has been forbidden.
27  PathForbidden(PathBuf),
28}
29
30type EventListener = Box<dyn Fn(&Event) + Send>;
31
32/// Scope for filesystem access.
33#[derive(Clone)]
34pub struct Scope {
35  allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
36  forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
37  event_listeners: Arc<Mutex<HashMap<ScopeEventId, EventListener>>>,
38  match_options: glob::MatchOptions,
39  next_event_id: Arc<AtomicU32>,
40}
41
42impl Scope {
43  fn next_event_id(&self) -> u32 {
44    self.next_event_id.fetch_add(1, Ordering::Relaxed)
45  }
46}
47
48impl fmt::Debug for Scope {
49  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50    f.debug_struct("Scope")
51      .field(
52        "allowed_patterns",
53        &self
54          .allowed_patterns
55          .lock()
56          .unwrap()
57          .iter()
58          .map(|p| p.as_str())
59          .collect::<Vec<&str>>(),
60      )
61      .field(
62        "forbidden_patterns",
63        &self
64          .forbidden_patterns
65          .lock()
66          .unwrap()
67          .iter()
68          .map(|p| p.as_str())
69          .collect::<Vec<&str>>(),
70      )
71      .finish()
72  }
73}
74
75fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
76  list: &mut HashSet<Pattern>,
77  pattern: P,
78  f: F,
79) -> crate::Result<()> {
80  // Reconstruct pattern path components with appropriate separator
81  // so `some\path/to/dir/**\*` would be `some/path/to/dir/**/*` on Unix
82  // and  `some\path\to\dir\**\*` on Windows.
83  let path: PathBuf = pattern.as_ref().components().collect();
84
85  // Add pattern as is to be matched with paths as is
86  let path_str = path.to_string_lossy();
87  list.insert(f(&path_str)?);
88
89  // On Windows, if path starts with a Prefix, try to strip it if possible
90  // so `\\?\C:\\SomeDir` would result in a scope of:
91  //   - `\\?\C:\\SomeDir`
92  //   - `C:\\SomeDir`
93  #[cfg(windows)]
94  {
95    use std::path::{Component, Prefix};
96
97    let mut components = path.components();
98
99    let is_unc = match components.next() {
100      Some(Component::Prefix(p)) => match p.kind() {
101        Prefix::VerbatimDisk(..) => true,
102        _ => false, // Other kinds of UNC paths
103      },
104      _ => false, // relative or empty
105    };
106
107    if is_unc {
108      // we remove UNC manually, instead of `dunce::simplified` because
109      // `path` could have `*` in it and that's not allowed on Windows and
110      // `dunce::simplified` will check that and return `path` as is without simplification
111      let simplified = path
112        .to_str()
113        .and_then(|s| s.get(4..))
114        .map_or(path.as_path(), Path::new);
115
116      let simplified_str = simplified.to_string_lossy();
117      if simplified_str != path_str {
118        list.insert(f(&simplified_str)?);
119      }
120    }
121  }
122
123  // Add canonicalized version of the pattern or canonicalized version of its parents
124  // so `/data/user/0/appid/assets/*` would be canonicalized to `/data/data/appid/assets/*`
125  // and can then be matched against any of them.
126  if let Some(p) = canonicalize_parent(path) {
127    list.insert(f(&p.to_string_lossy())?);
128  }
129
130  Ok(())
131}
132
133/// Attempt to canonicalize path or its parents in case we have a path like `/data/user/0/appid/**`
134/// where `**` obviously does not exist but we need to canonicalize the parent.
135///
136/// example: given the `/data/user/0/appid/assets/*` path,
137/// it's a glob pattern so it won't exist (std::fs::canonicalize() fails);
138///
139/// the second iteration needs to check `/data/user/0/appid/assets` and save the `*` component to append later.
140///
141/// if it also does not exist, a third iteration is required to check `/data/user/0/appid`
142/// with `assets/*` as the cached value (`checked_path` variable)
143/// on Android that gets canonicalized to `/data/data/appid` so the final value will be `/data/data/appid/assets/*`
144/// which is the value we want to check when we execute the `Scope::is_allowed` function
145fn canonicalize_parent(mut path: PathBuf) -> Option<PathBuf> {
146  let mut failed_components = None;
147
148  loop {
149    if let Ok(path) = path.canonicalize() {
150      break Some(if let Some(p) = failed_components {
151        path.join(p)
152      } else {
153        path
154      });
155    }
156
157    // grap the last component of the path
158    if let Some(mut last) = path.iter().next_back().map(PathBuf::from) {
159      // remove the last component of the path so the next iteration checks its parent
160      // if there is no more parent components, we failed to canonicalize
161      if !path.pop() {
162        break None;
163      }
164
165      // append the already checked path to the last component
166      // to construct `<last>/<checked_path>` and saved it for next iteration
167      if let Some(failed_components) = &failed_components {
168        last.push(failed_components);
169      }
170      failed_components.replace(last);
171    } else {
172      break None;
173    }
174  }
175}
176impl Scope {
177  /// Creates a new scope from a [`FsScope`] configuration.
178  pub fn new<R: crate::Runtime, M: crate::Manager<R>>(
179    manager: &M,
180    scope: &FsScope,
181  ) -> crate::Result<Self> {
182    let mut allowed_patterns = HashSet::new();
183    for path in scope.allowed_paths() {
184      if let Ok(path) = manager.path().parse(path) {
185        push_pattern(&mut allowed_patterns, path, Pattern::new)?;
186      }
187    }
188
189    let mut forbidden_patterns = HashSet::new();
190    if let Some(forbidden_paths) = scope.forbidden_paths() {
191      for path in forbidden_paths {
192        if let Ok(path) = manager.path().parse(path) {
193          push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
194        }
195      }
196    }
197
198    let require_literal_leading_dot = match scope {
199      FsScope::Scope {
200        require_literal_leading_dot: Some(require),
201        ..
202      } => *require,
203      // dotfiles are not supposed to be exposed by default on unix
204      #[cfg(unix)]
205      _ => true,
206      #[cfg(windows)]
207      _ => false,
208    };
209
210    Ok(Self {
211      allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
212      forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
213      event_listeners: Default::default(),
214      next_event_id: Default::default(),
215      match_options: glob::MatchOptions {
216        // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
217        // see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
218        require_literal_separator: true,
219        require_literal_leading_dot,
220        ..Default::default()
221      },
222    })
223  }
224
225  /// The list of allowed patterns.
226  pub fn allowed_patterns(&self) -> HashSet<Pattern> {
227    self.allowed_patterns.lock().unwrap().clone()
228  }
229
230  /// The list of forbidden patterns.
231  pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
232    self.forbidden_patterns.lock().unwrap().clone()
233  }
234
235  /// Listen to an event on this scope.
236  pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
237    let id = self.next_event_id();
238    self.listen_with_id(id, f);
239    id
240  }
241
242  fn listen_with_id<F: Fn(&Event) + Send + 'static>(&self, id: ScopeEventId, f: F) {
243    self.event_listeners.lock().unwrap().insert(id, Box::new(f));
244  }
245
246  /// Listen to an event on this scope and immediately unlisten.
247  pub fn once<F: FnOnce(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
248    let listerners = self.event_listeners.clone();
249    let handler = std::cell::Cell::new(Some(f));
250    let id = self.next_event_id();
251    self.listen_with_id(id, move |event| {
252      listerners.lock().unwrap().remove(&id);
253      let handler = handler
254        .take()
255        .expect("attempted to call handler more than once");
256      handler(event)
257    });
258    id
259  }
260
261  /// Removes an event listener on this scope.
262  pub fn unlisten(&self, id: ScopeEventId) {
263    self.event_listeners.lock().unwrap().remove(&id);
264  }
265
266  fn emit(&self, event: Event) {
267    let listeners = self.event_listeners.lock().unwrap();
268    let handlers = listeners.values();
269    for listener in handlers {
270      listener(&event);
271    }
272  }
273
274  /// Extend the allowed patterns with the given directory.
275  ///
276  /// After this function has been called, the frontend will be able to use the Tauri API to read
277  /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
278  pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
279    let path = path.as_ref();
280    {
281      let mut list = self.allowed_patterns.lock().unwrap();
282
283      // allow the directory to be read
284      push_pattern(&mut list, path, escaped_pattern)?;
285      // allow its files and subdirectories to be read
286      push_pattern(&mut list, path, |p| {
287        escaped_pattern_with(p, if recursive { "**" } else { "*" })
288      })?;
289    }
290    self.emit(Event::PathAllowed(path.to_path_buf()));
291    Ok(())
292  }
293
294  /// Extend the allowed patterns with the given file path.
295  ///
296  /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
297  pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
298    let path = path.as_ref();
299    push_pattern(
300      &mut self.allowed_patterns.lock().unwrap(),
301      path,
302      escaped_pattern,
303    )?;
304    self.emit(Event::PathAllowed(path.to_path_buf()));
305    Ok(())
306  }
307
308  /// Set the given directory path to be forbidden by this scope.
309  ///
310  /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
311  pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
312    let path = path.as_ref();
313    {
314      let mut list = self.forbidden_patterns.lock().unwrap();
315
316      // allow the directory to be read
317      push_pattern(&mut list, path, escaped_pattern)?;
318      // allow its files and subdirectories to be read
319      push_pattern(&mut list, path, |p| {
320        escaped_pattern_with(p, if recursive { "**" } else { "*" })
321      })?;
322    }
323    self.emit(Event::PathForbidden(path.to_path_buf()));
324    Ok(())
325  }
326
327  /// Set the given file path to be forbidden by this scope.
328  ///
329  /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
330  pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
331    let path = path.as_ref();
332    push_pattern(
333      &mut self.forbidden_patterns.lock().unwrap(),
334      path,
335      escaped_pattern,
336    )?;
337    self.emit(Event::PathForbidden(path.to_path_buf()));
338    Ok(())
339  }
340
341  /// Determines if the given path is allowed on this scope.
342  ///
343  /// Returns `false` if the path was explicitly forbidden or neither allowed nor forbidden.
344  ///
345  /// May return `false` if the path points to a broken symlink.
346  pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
347    let path = try_resolve_symlink_and_canonicalize(path);
348
349    if let Ok(path) = path {
350      let path: PathBuf = path.components().collect();
351      let forbidden = self
352        .forbidden_patterns
353        .lock()
354        .unwrap()
355        .iter()
356        .any(|p| p.matches_path_with(&path, self.match_options));
357
358      if forbidden {
359        false
360      } else {
361        let allowed = self
362          .allowed_patterns
363          .lock()
364          .unwrap()
365          .iter()
366          .any(|p| p.matches_path_with(&path, self.match_options));
367
368        allowed
369      }
370    } else {
371      false
372    }
373  }
374
375  /// Determines if the given path is explicitly forbidden on this scope.
376  ///
377  /// May return `true` if the path points to a broken symlink.
378  pub fn is_forbidden<P: AsRef<Path>>(&self, path: P) -> bool {
379    let path = try_resolve_symlink_and_canonicalize(path);
380
381    if let Ok(path) = path {
382      let path: PathBuf = path.components().collect();
383      self
384        .forbidden_patterns
385        .lock()
386        .unwrap()
387        .iter()
388        .any(|p| p.matches_path_with(&path, self.match_options))
389    } else {
390      true
391    }
392  }
393}
394
395fn try_resolve_symlink_and_canonicalize<P: AsRef<Path>>(path: P) -> crate::Result<PathBuf> {
396  let path = path.as_ref();
397  let path = if path.is_symlink() {
398    std::fs::read_link(path)?
399  } else {
400    path.to_path_buf()
401  };
402  if !path.exists() {
403    crate::Result::Ok(path)
404  } else {
405    std::fs::canonicalize(path).map_err(Into::into)
406  }
407}
408
409fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
410  Pattern::new(&glob::Pattern::escape(p))
411}
412
413fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
414  if p.ends_with(MAIN_SEPARATOR) {
415    Pattern::new(&format!("{}{append}", glob::Pattern::escape(p)))
416  } else {
417    Pattern::new(&format!(
418      "{}{}{append}",
419      glob::Pattern::escape(p),
420      MAIN_SEPARATOR
421    ))
422  }
423}
424
425#[cfg(test)]
426mod tests {
427  use std::collections::HashSet;
428
429  use glob::Pattern;
430
431  use super::{push_pattern, Scope};
432
433  fn new_scope() -> Scope {
434    Scope {
435      allowed_patterns: Default::default(),
436      forbidden_patterns: Default::default(),
437      event_listeners: Default::default(),
438      next_event_id: Default::default(),
439      match_options: glob::MatchOptions {
440        // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
441        // see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
442        require_literal_separator: true,
443        // dotfiles are not supposed to be exposed by default on unix
444        #[cfg(unix)]
445        require_literal_leading_dot: true,
446        #[cfg(windows)]
447        require_literal_leading_dot: false,
448        ..Default::default()
449      },
450    }
451  }
452
453  #[test]
454  fn path_is_escaped() {
455    let scope = new_scope();
456    #[cfg(unix)]
457    {
458      scope.allow_directory("/home/tauri/**", false).unwrap();
459      assert!(scope.is_allowed("/home/tauri/**"));
460      assert!(scope.is_allowed("/home/tauri/**/file"));
461      assert!(!scope.is_allowed("/home/tauri/anyfile"));
462    }
463    #[cfg(windows)]
464    {
465      scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
466      assert!(scope.is_allowed("C:\\home\\tauri\\**"));
467      assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
468      assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
469    }
470
471    let scope = new_scope();
472    #[cfg(unix)]
473    {
474      scope.allow_file("/home/tauri/**").unwrap();
475      assert!(scope.is_allowed("/home/tauri/**"));
476      assert!(!scope.is_allowed("/home/tauri/**/file"));
477      assert!(!scope.is_allowed("/home/tauri/anyfile"));
478    }
479    #[cfg(windows)]
480    {
481      scope.allow_file("C:\\home\\tauri\\**").unwrap();
482      assert!(scope.is_allowed("C:\\home\\tauri\\**"));
483      assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
484      assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
485    }
486
487    let scope = new_scope();
488    #[cfg(unix)]
489    {
490      scope.allow_directory("/home/tauri", true).unwrap();
491      scope.forbid_directory("/home/tauri/**", false).unwrap();
492      assert!(!scope.is_allowed("/home/tauri/**"));
493      assert!(!scope.is_allowed("/home/tauri/**/file"));
494      assert!(scope.is_allowed("/home/tauri/**/inner/file"));
495      assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
496      assert!(scope.is_allowed("/home/tauri/anyfile"));
497    }
498    #[cfg(windows)]
499    {
500      scope.allow_directory("C:\\home\\tauri", true).unwrap();
501      scope
502        .forbid_directory("C:\\home\\tauri\\**", false)
503        .unwrap();
504      assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
505      assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
506      assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
507      assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
508      assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
509    }
510
511    let scope = new_scope();
512    #[cfg(unix)]
513    {
514      scope.allow_directory("/home/tauri", true).unwrap();
515      scope.forbid_file("/home/tauri/**").unwrap();
516      assert!(!scope.is_allowed("/home/tauri/**"));
517      assert!(scope.is_allowed("/home/tauri/**/file"));
518      assert!(scope.is_allowed("/home/tauri/**/inner/file"));
519      assert!(scope.is_allowed("/home/tauri/anyfile"));
520    }
521    #[cfg(windows)]
522    {
523      scope.allow_directory("C:\\home\\tauri", true).unwrap();
524      scope.forbid_file("C:\\home\\tauri\\**").unwrap();
525      assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
526      assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
527      assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
528      assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
529    }
530
531    let scope = new_scope();
532    #[cfg(unix)]
533    {
534      scope.allow_directory("/home/tauri", false).unwrap();
535      assert!(scope.is_allowed("/home/tauri/**"));
536      assert!(!scope.is_allowed("/home/tauri/**/file"));
537      assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
538      assert!(scope.is_allowed("/home/tauri/anyfile"));
539    }
540    #[cfg(windows)]
541    {
542      scope.allow_directory("C:\\home\\tauri", false).unwrap();
543      assert!(scope.is_allowed("C:\\home\\tauri\\**"));
544      assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
545      assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
546      assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
547    }
548  }
549
550  #[cfg(windows)]
551  #[test]
552  fn windows_root_paths() {
553    let scope = new_scope();
554    {
555      // UNC network path
556      scope.allow_directory("\\\\localhost\\c$", true).unwrap();
557      assert!(scope.is_allowed("\\\\localhost\\c$"));
558      assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
559      assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile"));
560      assert!(!scope.is_allowed("\\\\localhost\\d$"));
561      assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
562    }
563
564    let scope = new_scope();
565    {
566      // Verbatim UNC network path
567      scope
568        .allow_directory("\\\\?\\UNC\\localhost\\c$", true)
569        .unwrap();
570      assert!(scope.is_allowed("\\\\localhost\\c$"));
571      assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
572      assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile"));
573      // A non-existent file cannot be canonicalized to a verbatim UNC path, so this will fail to match
574      assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile"));
575      assert!(!scope.is_allowed("\\\\localhost\\d$"));
576      assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
577    }
578
579    let scope = new_scope();
580    {
581      // Device namespace
582      scope.allow_file("\\\\.\\COM1").unwrap();
583      assert!(scope.is_allowed("\\\\.\\COM1"));
584      assert!(!scope.is_allowed("\\\\.\\COM2"));
585    }
586
587    let scope = new_scope();
588    {
589      // Disk root
590      scope.allow_directory("C:\\", true).unwrap();
591      assert!(scope.is_allowed("C:\\Windows"));
592      assert!(scope.is_allowed("C:\\Windows\\system.ini"));
593      assert!(scope.is_allowed("C:\\NonExistentFile"));
594      assert!(!scope.is_allowed("D:\\home"));
595    }
596
597    let scope = new_scope();
598    {
599      // Verbatim disk root
600      scope.allow_directory("\\\\?\\C:\\", true).unwrap();
601      assert!(scope.is_allowed("C:\\Windows"));
602      assert!(scope.is_allowed("C:\\Windows\\system.ini"));
603      assert!(scope.is_allowed("C:\\NonExistentFile"));
604      assert!(!scope.is_allowed("D:\\home"));
605    }
606
607    let scope = new_scope();
608    {
609      // Verbatim path
610      scope.allow_file("\\\\?\\anyfile").unwrap();
611      assert!(scope.is_allowed("\\\\?\\anyfile"));
612      assert!(!scope.is_allowed("\\\\?\\otherfile"));
613    }
614  }
615
616  #[test]
617  fn push_pattern_generated_paths() {
618    macro_rules! assert_pattern {
619      ($patterns:ident, $pattern:literal) => {
620        assert!($patterns.contains(&Pattern::new($pattern).unwrap()))
621      };
622    }
623
624    let mut patterns = HashSet::new();
625
626    #[cfg(not(windows))]
627    {
628      push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern");
629      push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern");
630
631      assert_pattern!(patterns, "/path/to/dir");
632      assert_pattern!(patterns, "/path/to/dir/**");
633    }
634
635    #[cfg(windows)]
636    {
637      push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new)
638        .expect("failed to push pattern");
639      push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new)
640        .expect("failed to push pattern");
641
642      assert_pattern!(patterns, "C:\\path\\to\\dir");
643      assert_pattern!(patterns, "C:\\path\\to\\dir\\**");
644      assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir");
645      assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
646    }
647  }
648}