-
Notifications
You must be signed in to change notification settings - Fork 287
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(fs): Escape paths in Scope methods #2070
Changes from 8 commits
b42064d
caac44d
f323eb8
0f2e408
e8fabf0
6b344c8
ad3156f
00b3d16
cc182fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,13 +16,13 @@ use std::{ | |
borrow::Cow, | ||
fs::File, | ||
io::{BufReader, Lines, 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 { | ||
|
@@ -967,6 +967,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(); | ||
|
@@ -999,40 +1001,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>>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this implemented separately here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i can implement it in tauri::fs::Scope too and re-use that but wanted to keep the plugin compatible with all tauri v2 versions for now. not too happy about the approach itself but merging instances of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's ship with this for now, but I think we should still add it in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, will open a pr later |
||
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 { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do really need to manually call
is_forbidden
, doesn'tscope.is_allowed
check for forbidden/denied patterns?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the problem is that is_allowed returns false for paths that are explictly denied and paths that are neither allowed nor denied.
When we're checking for 2 scopes at the same time we only care about the explicitly denied paths because those take precedence of explictly allowed paths.
A path that's neither allowed nor denied in the first scope could still be explicitly allowed by the second scope.
Alternatively a way to create a new Scope from 2 existing scopes would be cool too, then is_allowed would work for us here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can improve this a bit by adding the
Scope::is_forbidden
in tauri