fs.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. collections::{HashMap, HashSet},
  6. fmt,
  7. path::{Path, PathBuf, MAIN_SEPARATOR},
  8. sync::{Arc, Mutex},
  9. };
  10. pub use glob::Pattern;
  11. use tauri_utils::{
  12. config::{Config, FsAllowlistScope},
  13. Env, PackageInfo,
  14. };
  15. use uuid::Uuid;
  16. use crate::api::path::parse as parse_path;
  17. /// Scope change event.
  18. #[derive(Debug, Clone)]
  19. pub enum Event {
  20. /// A path has been allowed.
  21. PathAllowed(PathBuf),
  22. /// A path has been forbidden.
  23. PathForbidden(PathBuf),
  24. }
  25. type EventListener = Box<dyn Fn(&Event) + Send>;
  26. /// Scope for filesystem access.
  27. #[derive(Clone)]
  28. pub struct Scope {
  29. alllowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
  30. forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
  31. event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
  32. }
  33. impl fmt::Debug for Scope {
  34. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  35. f.debug_struct("Scope")
  36. .field(
  37. "alllowed_patterns",
  38. &self
  39. .alllowed_patterns
  40. .lock()
  41. .unwrap()
  42. .iter()
  43. .map(|p| p.as_str())
  44. .collect::<Vec<&str>>(),
  45. )
  46. .field(
  47. "forbidden_patterns",
  48. &self
  49. .forbidden_patterns
  50. .lock()
  51. .unwrap()
  52. .iter()
  53. .map(|p| p.as_str())
  54. .collect::<Vec<&str>>(),
  55. )
  56. .finish()
  57. }
  58. }
  59. fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
  60. list: &mut HashSet<Pattern>,
  61. pattern: P,
  62. f: F,
  63. ) -> crate::Result<()> {
  64. let path: PathBuf = pattern.as_ref().components().collect();
  65. list.insert(f(&path.to_string_lossy())?);
  66. #[cfg(windows)]
  67. {
  68. if let Ok(p) = std::fs::canonicalize(&path) {
  69. list.insert(f(&p.to_string_lossy())?);
  70. } else {
  71. list.insert(f(&format!("\\\\?\\{}", path.display()))?);
  72. }
  73. }
  74. Ok(())
  75. }
  76. impl Scope {
  77. /// Creates a new scope from a `FsAllowlistScope` configuration.
  78. pub(crate) fn for_fs_api(
  79. config: &Config,
  80. package_info: &PackageInfo,
  81. env: &Env,
  82. scope: &FsAllowlistScope,
  83. ) -> crate::Result<Self> {
  84. let mut alllowed_patterns = HashSet::new();
  85. for path in scope.allowed_paths() {
  86. if let Ok(path) = parse_path(config, package_info, env, path) {
  87. push_pattern(&mut alllowed_patterns, path, Pattern::new)?;
  88. }
  89. }
  90. let mut forbidden_patterns = HashSet::new();
  91. if let Some(forbidden_paths) = scope.forbidden_paths() {
  92. for path in forbidden_paths {
  93. if let Ok(path) = parse_path(config, package_info, env, path) {
  94. push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
  95. }
  96. }
  97. }
  98. Ok(Self {
  99. alllowed_patterns: Arc::new(Mutex::new(alllowed_patterns)),
  100. forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
  101. event_listeners: Default::default(),
  102. })
  103. }
  104. /// The list of allowed patterns.
  105. pub fn allowed_patterns(&self) -> HashSet<Pattern> {
  106. self.alllowed_patterns.lock().unwrap().clone()
  107. }
  108. /// The list of forbidden patterns.
  109. pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
  110. self.forbidden_patterns.lock().unwrap().clone()
  111. }
  112. /// Listen to an event on this scope.
  113. pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
  114. let id = Uuid::new_v4();
  115. self.event_listeners.lock().unwrap().insert(id, Box::new(f));
  116. id
  117. }
  118. fn trigger(&self, event: Event) {
  119. let listeners = self.event_listeners.lock().unwrap();
  120. let handlers = listeners.values();
  121. for listener in handlers {
  122. listener(&event);
  123. }
  124. }
  125. /// Extend the allowed patterns with the given directory.
  126. ///
  127. /// After this function has been called, the frontend will be able to use the Tauri API to read
  128. /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
  129. pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
  130. let path = path.as_ref();
  131. {
  132. let mut list = self.alllowed_patterns.lock().unwrap();
  133. // allow the directory to be read
  134. push_pattern(&mut list, &path, escaped_pattern)?;
  135. // allow its files and subdirectories to be read
  136. push_pattern(&mut list, &path, |p| {
  137. escaped_pattern_with(p, if recursive { "**" } else { "*" })
  138. })?;
  139. }
  140. self.trigger(Event::PathAllowed(path.to_path_buf()));
  141. Ok(())
  142. }
  143. /// Extend the allowed patterns with the given file path.
  144. ///
  145. /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
  146. pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
  147. let path = path.as_ref();
  148. push_pattern(
  149. &mut self.alllowed_patterns.lock().unwrap(),
  150. &path,
  151. escaped_pattern,
  152. )?;
  153. self.trigger(Event::PathAllowed(path.to_path_buf()));
  154. Ok(())
  155. }
  156. /// Set the given directory path to be forbidden by this scope.
  157. ///
  158. /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
  159. pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
  160. let path = path.as_ref();
  161. {
  162. let mut list = self.forbidden_patterns.lock().unwrap();
  163. // allow the directory to be read
  164. push_pattern(&mut list, &path, escaped_pattern)?;
  165. // allow its files and subdirectories to be read
  166. push_pattern(&mut list, &path, |p| {
  167. escaped_pattern_with(p, if recursive { "**" } else { "*" })
  168. })?;
  169. }
  170. self.trigger(Event::PathForbidden(path.to_path_buf()));
  171. Ok(())
  172. }
  173. /// Set the given file path to be forbidden by this scope.
  174. ///
  175. /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
  176. pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
  177. let path = path.as_ref();
  178. push_pattern(
  179. &mut self.forbidden_patterns.lock().unwrap(),
  180. &path,
  181. escaped_pattern,
  182. )?;
  183. self.trigger(Event::PathForbidden(path.to_path_buf()));
  184. Ok(())
  185. }
  186. /// Determines if the given path is allowed on this scope.
  187. pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
  188. let path = path.as_ref();
  189. let path = if !path.exists() {
  190. crate::Result::Ok(path.to_path_buf())
  191. } else {
  192. std::fs::canonicalize(path).map_err(Into::into)
  193. };
  194. if let Ok(path) = path {
  195. let path: PathBuf = path.components().collect();
  196. let options = glob::MatchOptions {
  197. // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
  198. // see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5
  199. require_literal_separator: true,
  200. // dotfiles are not supposed to be exposed by default
  201. #[cfg(unix)]
  202. require_literal_leading_dot: true,
  203. ..Default::default()
  204. };
  205. let forbidden = self
  206. .forbidden_patterns
  207. .lock()
  208. .unwrap()
  209. .iter()
  210. .any(|p| p.matches_path_with(&path, options));
  211. if forbidden {
  212. false
  213. } else {
  214. let allowed = self
  215. .alllowed_patterns
  216. .lock()
  217. .unwrap()
  218. .iter()
  219. .any(|p| p.matches_path_with(&path, options));
  220. allowed
  221. }
  222. } else {
  223. false
  224. }
  225. }
  226. }
  227. fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
  228. Pattern::new(&glob::Pattern::escape(p))
  229. }
  230. fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
  231. Pattern::new(&format!(
  232. "{}{}{}",
  233. glob::Pattern::escape(p),
  234. MAIN_SEPARATOR,
  235. append
  236. ))
  237. }
  238. #[cfg(test)]
  239. mod tests {
  240. use super::Scope;
  241. fn new_scope() -> Scope {
  242. Scope {
  243. alllowed_patterns: Default::default(),
  244. forbidden_patterns: Default::default(),
  245. event_listeners: Default::default(),
  246. }
  247. }
  248. #[test]
  249. fn path_is_escaped() {
  250. let scope = new_scope();
  251. #[cfg(unix)]
  252. {
  253. scope.allow_directory("/home/tauri/**", false).unwrap();
  254. assert!(scope.is_allowed("/home/tauri/**"));
  255. assert!(scope.is_allowed("/home/tauri/**/file"));
  256. assert!(!scope.is_allowed("/home/tauri/anyfile"));
  257. }
  258. #[cfg(windows)]
  259. {
  260. scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
  261. assert!(scope.is_allowed("C:\\home\\tauri\\**"));
  262. assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
  263. assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
  264. }
  265. let scope = new_scope();
  266. #[cfg(unix)]
  267. {
  268. scope.allow_file("/home/tauri/**").unwrap();
  269. assert!(scope.is_allowed("/home/tauri/**"));
  270. assert!(!scope.is_allowed("/home/tauri/**/file"));
  271. assert!(!scope.is_allowed("/home/tauri/anyfile"));
  272. }
  273. #[cfg(windows)]
  274. {
  275. scope.allow_file("C:\\home\\tauri\\**").unwrap();
  276. assert!(scope.is_allowed("C:\\home\\tauri\\**"));
  277. assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
  278. assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
  279. }
  280. let scope = new_scope();
  281. #[cfg(unix)]
  282. {
  283. scope.allow_directory("/home/tauri", true).unwrap();
  284. scope.forbid_directory("/home/tauri/**", false).unwrap();
  285. assert!(!scope.is_allowed("/home/tauri/**"));
  286. assert!(!scope.is_allowed("/home/tauri/**/file"));
  287. assert!(scope.is_allowed("/home/tauri/**/inner/file"));
  288. assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
  289. assert!(scope.is_allowed("/home/tauri/anyfile"));
  290. }
  291. #[cfg(windows)]
  292. {
  293. scope.allow_directory("C:\\home\\tauri", true).unwrap();
  294. scope
  295. .forbid_directory("C:\\home\\tauri\\**", false)
  296. .unwrap();
  297. assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
  298. assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
  299. assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
  300. assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
  301. assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
  302. }
  303. let scope = new_scope();
  304. #[cfg(unix)]
  305. {
  306. scope.allow_directory("/home/tauri", true).unwrap();
  307. scope.forbid_file("/home/tauri/**").unwrap();
  308. assert!(!scope.is_allowed("/home/tauri/**"));
  309. assert!(scope.is_allowed("/home/tauri/**/file"));
  310. assert!(scope.is_allowed("/home/tauri/**/inner/file"));
  311. assert!(scope.is_allowed("/home/tauri/anyfile"));
  312. }
  313. #[cfg(windows)]
  314. {
  315. scope.allow_directory("C:\\home\\tauri", true).unwrap();
  316. scope.forbid_file("C:\\home\\tauri\\**").unwrap();
  317. assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
  318. assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
  319. assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
  320. assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
  321. }
  322. let scope = new_scope();
  323. #[cfg(unix)]
  324. {
  325. scope.allow_directory("/home/tauri", false).unwrap();
  326. assert!(scope.is_allowed("/home/tauri/**"));
  327. assert!(!scope.is_allowed("/home/tauri/**/file"));
  328. assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
  329. assert!(scope.is_allowed("/home/tauri/anyfile"));
  330. }
  331. #[cfg(windows)]
  332. {
  333. scope.allow_directory("C:\\home\\tauri", false).unwrap();
  334. assert!(scope.is_allowed("C:\\home\\tauri\\**"));
  335. assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
  336. assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
  337. assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
  338. }
  339. }
  340. }