fs.rs 13 KB

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