Skip to content

Commit fecfd55

Browse files
authored
fix(fs)!: use tauri::scope::fs::Scope (tauri-apps#2070)
1 parent ed98102 commit fecfd55

File tree

7 files changed

+120
-251
lines changed

7 files changed

+120
-251
lines changed

.changes/fix-fs-scope-escape-paths.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fs: minor
3+
persisted-scope: minor
4+
---
5+
6+
**Breaking Change:** Replaced the custom `tauri_plugin_fs::Scope` struct with `tauri::fs::Scope`.

plugins/dialog/src/commands.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub(crate) async fn open<R: Runtime>(
143143
for folder in folders {
144144
if let Ok(path) = folder.clone().into_path() {
145145
if let Some(s) = window.try_fs_scope() {
146-
s.allow_directory(&path, options.recursive);
146+
s.allow_directory(&path, options.recursive)?;
147147
}
148148
tauri_scope.allow_directory(&path, options.directory)?;
149149
}
@@ -157,7 +157,7 @@ pub(crate) async fn open<R: Runtime>(
157157
if let Some(folder) = &folder {
158158
if let Ok(path) = folder.clone().into_path() {
159159
if let Some(s) = window.try_fs_scope() {
160-
s.allow_directory(&path, options.recursive);
160+
s.allow_directory(&path, options.recursive)?;
161161
}
162162
tauri_scope.allow_directory(&path, options.directory)?;
163163
}
@@ -175,7 +175,7 @@ pub(crate) async fn open<R: Runtime>(
175175
for file in files {
176176
if let Ok(path) = file.clone().into_path() {
177177
if let Some(s) = window.try_fs_scope() {
178-
s.allow_file(&path);
178+
s.allow_file(&path)?;
179179
}
180180

181181
tauri_scope.allow_file(&path)?;
@@ -190,7 +190,7 @@ pub(crate) async fn open<R: Runtime>(
190190
if let Some(file) = &file {
191191
if let Ok(path) = file.clone().into_path() {
192192
if let Some(s) = window.try_fs_scope() {
193-
s.allow_file(&path);
193+
s.allow_file(&path)?;
194194
}
195195
tauri_scope.allow_file(&path)?;
196196
}
@@ -232,7 +232,7 @@ pub(crate) async fn save<R: Runtime>(
232232
if let Some(p) = &path {
233233
if let Ok(path) = p.clone().into_path() {
234234
if let Some(s) = window.try_fs_scope() {
235-
s.allow_file(&path);
235+
s.allow_file(&path)?;
236236
}
237237
tauri_scope.allow_file(&path)?;
238238
}

plugins/fs/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ rustc-args = ["--cfg", "docsrs"]
1414
rustdoc-args = ["--cfg", "docsrs"]
1515

1616
[package.metadata.platforms.support]
17-
windows = { level = "full", notes = "" }
17+
windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write acces in `$RESOURCES` folder" }
1818
linux = { level = "full", notes = "No write access to `$RESOURCES` folder" }
1919
macos = { level = "full", notes = "No write access to `$RESOURCES` folder" }
2020
android = { level = "partial", notes = "Access is restricted to Application folder by default" }

plugins/fs/src/commands.rs

+63-20
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ use std::{
1616
borrow::Cow,
1717
fs::File,
1818
io::{BufRead, BufReader, Read, Write},
19-
path::PathBuf,
19+
path::{Path, PathBuf},
2020
str::FromStr,
2121
sync::Mutex,
2222
time::{SystemTime, UNIX_EPOCH},
2323
};
2424

25-
use crate::{scope::Entry, Error, FsExt, SafeFilePath};
25+
use crate::{scope::Entry, Error, SafeFilePath};
2626

2727
#[derive(Debug, thiserror::Error)]
2828
pub enum CommandError {
@@ -942,6 +942,8 @@ pub fn resolve_file<R: Runtime>(
942942
path: SafeFilePath,
943943
open_options: OpenOptions,
944944
) -> CommandResult<(File, PathBuf)> {
945+
use crate::FsExt;
946+
945947
match path {
946948
SafeFilePath::Url(url) => {
947949
let path = url.as_str().into();
@@ -974,40 +976,81 @@ pub fn resolve_path<R: Runtime>(
974976
path
975977
};
976978

979+
let fs_scope = webview.state::<crate::Scope>();
980+
977981
let scope = tauri::scope::fs::Scope::new(
978982
webview,
979983
&FsScope::Scope {
980-
allow: webview
981-
.fs_scope()
982-
.allowed
983-
.lock()
984-
.unwrap()
985-
.clone()
986-
.into_iter()
987-
.chain(global_scope.allows().iter().filter_map(|e| e.path.clone()))
984+
allow: global_scope
985+
.allows()
986+
.iter()
987+
.filter_map(|e| e.path.clone())
988988
.chain(command_scope.allows().iter().filter_map(|e| e.path.clone()))
989989
.collect(),
990-
deny: webview
991-
.fs_scope()
992-
.denied
993-
.lock()
994-
.unwrap()
995-
.clone()
996-
.into_iter()
997-
.chain(global_scope.denies().iter().filter_map(|e| e.path.clone()))
990+
deny: global_scope
991+
.denies()
992+
.iter()
993+
.filter_map(|e| e.path.clone())
998994
.chain(command_scope.denies().iter().filter_map(|e| e.path.clone()))
999995
.collect(),
1000-
require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
996+
require_literal_leading_dot: fs_scope.require_literal_leading_dot,
1001997
},
1002998
)?;
1003999

1004-
if scope.is_allowed(&path) {
1000+
let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix));
1001+
1002+
if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot)
1003+
|| is_forbidden(&scope, &path, require_literal_leading_dot)
1004+
{
1005+
return Err(CommandError::Plugin(Error::PathForbidden(path)));
1006+
}
1007+
1008+
if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
10051009
Ok(path)
10061010
} else {
10071011
Err(CommandError::Plugin(Error::PathForbidden(path)))
10081012
}
10091013
}
10101014

1015+
fn is_forbidden<P: AsRef<Path>>(
1016+
scope: &tauri::fs::Scope,
1017+
path: P,
1018+
require_literal_leading_dot: bool,
1019+
) -> bool {
1020+
let path = path.as_ref();
1021+
let path = if path.is_symlink() {
1022+
match std::fs::read_link(path) {
1023+
Ok(p) => p,
1024+
Err(_) => return false,
1025+
}
1026+
} else {
1027+
path.to_path_buf()
1028+
};
1029+
let path = if !path.exists() {
1030+
crate::Result::Ok(path)
1031+
} else {
1032+
std::fs::canonicalize(path).map_err(Into::into)
1033+
};
1034+
1035+
if let Ok(path) = path {
1036+
let path: PathBuf = path.components().collect();
1037+
scope.forbidden_patterns().iter().any(|p| {
1038+
p.matches_path_with(
1039+
&path,
1040+
glob::MatchOptions {
1041+
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
1042+
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
1043+
require_literal_separator: true,
1044+
require_literal_leading_dot,
1045+
..Default::default()
1046+
},
1047+
)
1048+
})
1049+
} else {
1050+
false
1051+
}
1052+
}
1053+
10111054
struct StdFileResource(Mutex<File>);
10121055

10131056
impl StdFileResource {

plugins/fs/src/lib.rs

+21-15
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use serde::Deserialize;
1515
use tauri::{
1616
ipc::ScopeObject,
1717
plugin::{Builder as PluginBuilder, TauriPlugin},
18-
utils::acl::Value,
18+
utils::{acl::Value, config::FsScope},
1919
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
2020
};
2121

@@ -39,7 +39,6 @@ pub use desktop::Fs;
3939
pub use mobile::Fs;
4040

4141
pub use error::Error;
42-
pub use scope::{Event as ScopeEvent, Scope};
4342

4443
pub use file_path::FilePath;
4544
pub use file_path::SafeFilePath;
@@ -365,21 +364,26 @@ impl ScopeObject for scope::Entry {
365364
}
366365
}
367366

367+
pub(crate) struct Scope {
368+
pub(crate) scope: tauri::fs::Scope,
369+
pub(crate) require_literal_leading_dot: Option<bool>,
370+
}
371+
368372
pub trait FsExt<R: Runtime> {
369-
fn fs_scope(&self) -> &Scope;
370-
fn try_fs_scope(&self) -> Option<&Scope>;
373+
fn fs_scope(&self) -> tauri::fs::Scope;
374+
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
371375

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

376380
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
377-
fn fs_scope(&self) -> &Scope {
378-
self.state::<Scope>().inner()
381+
fn fs_scope(&self) -> tauri::fs::Scope {
382+
self.state::<Scope>().scope.clone()
379383
}
380384

381-
fn try_fs_scope(&self) -> Option<&Scope> {
382-
self.try_state::<Scope>().map(|s| s.inner())
385+
fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
386+
self.try_state::<Scope>().map(|s| s.scope.clone())
383387
}
384388

385389
fn fs(&self) -> &Fs<R> {
@@ -419,11 +423,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
419423
watcher::unwatch
420424
])
421425
.setup(|app, api| {
422-
let mut scope = Scope::default();
423-
scope.require_literal_leading_dot = api
424-
.config()
425-
.as_ref()
426-
.and_then(|c| c.require_literal_leading_dot);
426+
let scope = Scope {
427+
require_literal_leading_dot: api
428+
.config()
429+
.as_ref()
430+
.and_then(|c| c.require_literal_leading_dot),
431+
scope: tauri::fs::Scope::new(app, &FsScope::default())?,
432+
};
427433

428434
#[cfg(target_os = "android")]
429435
{
@@ -446,9 +452,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
446452
let scope = app.fs_scope();
447453
for path in paths {
448454
if path.is_file() {
449-
scope.allow_file(path);
455+
let _ = scope.allow_file(path);
450456
} else {
451-
scope.allow_directory(path, true);
457+
let _ = scope.allow_directory(path, true);
452458
}
453459
}
454460
}

0 commit comments

Comments
 (0)