Skip to content

Commit

Permalink
fix(fs)!: use tauri::scope::fs::Scope (#2070)
Browse files Browse the repository at this point in the history
  • Loading branch information
FabianLars authored Nov 21, 2024
1 parent ed98102 commit fecfd55
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 251 deletions.
6 changes: 6 additions & 0 deletions .changes/fix-fs-scope-escape-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fs: minor
persisted-scope: minor
---

**Breaking Change:** Replaced the custom `tauri_plugin_fs::Scope` struct with `tauri::fs::Scope`.
10 changes: 5 additions & 5 deletions plugins/dialog/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ pub(crate) async fn open<R: Runtime>(
for folder in folders {
if let Ok(path) = folder.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(&path, options.recursive);
s.allow_directory(&path, options.recursive)?;
}
tauri_scope.allow_directory(&path, options.directory)?;
}
Expand All @@ -157,7 +157,7 @@ pub(crate) async fn open<R: Runtime>(
if let Some(folder) = &folder {
if let Ok(path) = folder.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(&path, options.recursive);
s.allow_directory(&path, options.recursive)?;
}
tauri_scope.allow_directory(&path, options.directory)?;
}
Expand All @@ -175,7 +175,7 @@ pub(crate) async fn open<R: Runtime>(
for file in files {
if let Ok(path) = file.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}

tauri_scope.allow_file(&path)?;
Expand All @@ -190,7 +190,7 @@ pub(crate) async fn open<R: Runtime>(
if let Some(file) = &file {
if let Ok(path) = file.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}
tauri_scope.allow_file(&path)?;
}
Expand Down Expand Up @@ -232,7 +232,7 @@ pub(crate) async fn save<R: Runtime>(
if let Some(p) = &path {
if let Ok(path) = p.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}
tauri_scope.allow_file(&path)?;
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]

[package.metadata.platforms.support]
windows = { level = "full", notes = "" }
windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write acces in `$RESOURCES` folder" }
linux = { level = "full", notes = "No write access to `$RESOURCES` folder" }
macos = { level = "full", notes = "No write access to `$RESOURCES` folder" }
android = { level = "partial", notes = "Access is restricted to Application folder by default" }
Expand Down
83 changes: 63 additions & 20 deletions plugins/fs/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ use std::{
borrow::Cow,
fs::File,
io::{BufRead, BufReader, Read, Write},
path::PathBuf,
path::{Path, PathBuf},
str::FromStr,
sync::Mutex,
time::{SystemTime, UNIX_EPOCH},
};

use crate::{scope::Entry, Error, FsExt, SafeFilePath};
use crate::{scope::Entry, Error, SafeFilePath};

#[derive(Debug, thiserror::Error)]
pub enum CommandError {
Expand Down Expand Up @@ -942,6 +942,8 @@ pub fn resolve_file<R: Runtime>(
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
use crate::FsExt;

match path {
SafeFilePath::Url(url) => {
let path = url.as_str().into();
Expand Down Expand Up @@ -974,40 +976,81 @@ pub fn resolve_path<R: Runtime>(
path
};

let fs_scope = webview.state::<crate::Scope>();

let scope = tauri::scope::fs::Scope::new(
webview,
&FsScope::Scope {
allow: webview
.fs_scope()
.allowed
.lock()
.unwrap()
.clone()
.into_iter()
.chain(global_scope.allows().iter().filter_map(|e| e.path.clone()))
allow: global_scope
.allows()
.iter()
.filter_map(|e| e.path.clone())
.chain(command_scope.allows().iter().filter_map(|e| e.path.clone()))
.collect(),
deny: webview
.fs_scope()
.denied
.lock()
.unwrap()
.clone()
.into_iter()
.chain(global_scope.denies().iter().filter_map(|e| e.path.clone()))
deny: global_scope
.denies()
.iter()
.filter_map(|e| e.path.clone())
.chain(command_scope.denies().iter().filter_map(|e| e.path.clone()))
.collect(),
require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
require_literal_leading_dot: fs_scope.require_literal_leading_dot,
},
)?;

if scope.is_allowed(&path) {
let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix));

if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot)
|| is_forbidden(&scope, &path, require_literal_leading_dot)
{
return Err(CommandError::Plugin(Error::PathForbidden(path)));
}

if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
Ok(path)
} else {
Err(CommandError::Plugin(Error::PathForbidden(path)))
}
}

fn is_forbidden<P: AsRef<Path>>(
scope: &tauri::fs::Scope,
path: P,
require_literal_leading_dot: bool,
) -> bool {
let path = path.as_ref();
let path = if path.is_symlink() {
match std::fs::read_link(path) {
Ok(p) => p,
Err(_) => return false,
}
} else {
path.to_path_buf()
};
let path = if !path.exists() {
crate::Result::Ok(path)
} else {
std::fs::canonicalize(path).map_err(Into::into)
};

if let Ok(path) = path {
let path: PathBuf = path.components().collect();
scope.forbidden_patterns().iter().any(|p| {
p.matches_path_with(
&path,
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,
require_literal_leading_dot,
..Default::default()
},
)
})
} else {
false
}
}

struct StdFileResource(Mutex<File>);

impl StdFileResource {
Expand Down
36 changes: 21 additions & 15 deletions plugins/fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use serde::Deserialize;
use tauri::{
ipc::ScopeObject,
plugin::{Builder as PluginBuilder, TauriPlugin},
utils::acl::Value,
utils::{acl::Value, config::FsScope},
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
};

Expand All @@ -39,7 +39,6 @@ pub use desktop::Fs;
pub use mobile::Fs;

pub use error::Error;
pub use scope::{Event as ScopeEvent, Scope};

pub use file_path::FilePath;
pub use file_path::SafeFilePath;
Expand Down Expand Up @@ -365,21 +364,26 @@ impl ScopeObject for scope::Entry {
}
}

pub(crate) struct Scope {
pub(crate) scope: tauri::fs::Scope,
pub(crate) require_literal_leading_dot: Option<bool>,
}

pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> &Scope;
fn try_fs_scope(&self) -> Option<&Scope>;
fn fs_scope(&self) -> tauri::fs::Scope;
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;

/// Cross platform file system APIs that also support manipulating Android files.
fn fs(&self) -> &Fs<R>;
}

impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
fn fs_scope(&self) -> &Scope {
self.state::<Scope>().inner()
fn fs_scope(&self) -> tauri::fs::Scope {
self.state::<Scope>().scope.clone()
}

fn try_fs_scope(&self) -> Option<&Scope> {
self.try_state::<Scope>().map(|s| s.inner())
fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
self.try_state::<Scope>().map(|s| s.scope.clone())
}

fn fs(&self) -> &Fs<R> {
Expand Down Expand Up @@ -419,11 +423,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
watcher::unwatch
])
.setup(|app, api| {
let mut scope = Scope::default();
scope.require_literal_leading_dot = api
.config()
.as_ref()
.and_then(|c| c.require_literal_leading_dot);
let scope = Scope {
require_literal_leading_dot: api
.config()
.as_ref()
.and_then(|c| c.require_literal_leading_dot),
scope: tauri::fs::Scope::new(app, &FsScope::default())?,
};

#[cfg(target_os = "android")]
{
Expand All @@ -446,9 +452,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
let scope = app.fs_scope();
for path in paths {
if path.is_file() {
scope.allow_file(path);
let _ = scope.allow_file(path);
} else {
scope.allow_directory(path, true);
let _ = scope.allow_directory(path, true);
}
}
}
Expand Down
Loading

0 comments on commit fecfd55

Please sign in to comment.