Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): escape glob characters in drop/dialogs , closes #5234 #5237

Merged
merged 5 commits into from Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/escape-pattern.md
@@ -0,0 +1,5 @@
---
"tauri": "patch"
---

Escape glob special characters in files/directories when dropping files or using the open/save dialogs.
108 changes: 91 additions & 17 deletions core/tauri/src/scope/fs.rs
Expand Up @@ -5,7 +5,7 @@
use std::{
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf},
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::{Arc, Mutex},
};

Expand Down Expand Up @@ -64,15 +64,19 @@ impl fmt::Debug for Scope {
}
}

fn push_pattern<P: AsRef<Path>>(list: &mut HashSet<Pattern>, pattern: P) -> crate::Result<()> {
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(Pattern::new(&path.to_string_lossy())?);
list.insert(f(&path.to_string_lossy())?);
#[cfg(windows)]
{
if let Ok(p) = std::fs::canonicalize(&path) {
list.insert(Pattern::new(&p.to_string_lossy())?);
list.insert(f(&p.to_string_lossy())?);
} else {
list.insert(Pattern::new(&format!("\\\\?\\{}", path.display()))?);
list.insert(f(&format!("\\\\?\\{}", path.display()))?);
}
}
Ok(())
Expand All @@ -89,15 +93,15 @@ impl Scope {
let mut allowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = parse_path(config, package_info, env, path) {
push_pattern(&mut allowed_patterns, path)?;
push_pattern(&mut allowed_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)?;
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
}
}
}
Expand Down Expand Up @@ -139,16 +143,18 @@ 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 and subdirectories.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref().to_path_buf();
let path = path.as_ref();
{
let mut list = self.allowed_patterns.lock().unwrap();

// allow the directory to be read
push_pattern(&mut list, &path)?;
push_pattern(&mut list, &path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
push_pattern(&mut list, &path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.trigger(Event::PathAllowed(path));
self.trigger(Event::PathAllowed(path.to_path_buf()));
Ok(())
}

Expand All @@ -157,7 +163,11 @@ impl Scope {
/// 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.allowed_patterns.lock().unwrap(), &path)?;
push_pattern(
&mut self.allowed_patterns.lock().unwrap(),
&path,
escaped_pattern,
)?;
self.trigger(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
Expand All @@ -166,16 +176,18 @@ impl 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().to_path_buf();
let path = path.as_ref();
{
let mut list = self.forbidden_patterns.lock().unwrap();

// allow the directory to be read
push_pattern(&mut list, &path)?;
push_pattern(&mut list, &path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
push_pattern(&mut list, &path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.trigger(Event::PathForbidden(path));
self.trigger(Event::PathForbidden(path.to_path_buf()));
Ok(())
}

Expand All @@ -184,7 +196,11 @@ impl 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)?;
push_pattern(
&mut self.forbidden_patterns.lock().unwrap(),
&path,
escaped_pattern,
)?;
self.trigger(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
Expand Down Expand Up @@ -224,3 +240,61 @@ impl Scope {
}
}
}

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 {
allowed_patterns: Default::default(),
forbidden_patterns: Default::default(),
event_listeners: Default::default(),
}
}

#[test]
fn path_is_escaped() {
let scope = new_scope();
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"));

let scope = new_scope();
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"));

let scope = new_scope();
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"));

let scope = new_scope();
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"));
}
}