Skip to content

Commit 4d89f60

Browse files
committed
refactor(core): prevent path traversal [TRI-012] (#35)
1 parent d4db95e commit 4d89f60

2 files changed

Lines changed: 78 additions & 35 deletions

File tree

.changes/prevent-path-traversal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Prevent path traversal on the file system APIs.

core/tauri/src/endpoints/file_system.rs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,47 @@ use crate::{
99
};
1010

1111
use super::InvokeContext;
12-
use serde::{Deserialize, Serialize};
12+
use serde::{
13+
de::{Deserializer, Error as DeError},
14+
Deserialize, Serialize,
15+
};
1316
use tauri_macros::{module_command_handler, CommandModule};
1417

1518
use std::{
1619
fs,
1720
fs::File,
1821
io::Write,
19-
path::{Path, PathBuf},
22+
path::{Component, Path},
2023
sync::Arc,
2124
};
2225

26+
pub struct SafePathBuf(std::path::PathBuf);
27+
28+
impl AsRef<Path> for SafePathBuf {
29+
fn as_ref(&self) -> &Path {
30+
self.0.as_ref()
31+
}
32+
}
33+
34+
impl<'de> Deserialize<'de> for SafePathBuf {
35+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36+
where
37+
D: Deserializer<'de>,
38+
{
39+
let path = std::path::PathBuf::deserialize(deserializer)?;
40+
if path.components().any(|x| {
41+
matches!(
42+
x,
43+
Component::ParentDir | Component::RootDir | Component::Prefix(_)
44+
)
45+
}) {
46+
Err(DeError::custom("cannot traverse directory"))
47+
} else {
48+
Ok(SafePathBuf(path))
49+
}
50+
}
51+
}
52+
2353
/// The options for the directory functions on the file system API.
2454
#[derive(Debug, Clone, Deserialize)]
2555
pub struct DirOperationOptions {
@@ -45,46 +75,46 @@ pub struct FileOperationOptions {
4575
pub enum Cmd {
4676
/// The read text file API.
4777
ReadFile {
48-
path: PathBuf,
78+
path: SafePathBuf,
4979
options: Option<FileOperationOptions>,
5080
},
5181
/// The write file API.
5282
WriteFile {
53-
path: PathBuf,
83+
path: SafePathBuf,
5484
contents: Vec<u8>,
5585
options: Option<FileOperationOptions>,
5686
},
5787
/// The read dir API.
5888
ReadDir {
59-
path: PathBuf,
89+
path: SafePathBuf,
6090
options: Option<DirOperationOptions>,
6191
},
6292
/// The copy file API.
6393
CopyFile {
64-
source: PathBuf,
65-
destination: PathBuf,
94+
source: SafePathBuf,
95+
destination: SafePathBuf,
6696
options: Option<FileOperationOptions>,
6797
},
6898
/// The create dir API.
6999
CreateDir {
70-
path: PathBuf,
100+
path: SafePathBuf,
71101
options: Option<DirOperationOptions>,
72102
},
73103
/// The remove dir API.
74104
RemoveDir {
75-
path: PathBuf,
105+
path: SafePathBuf,
76106
options: Option<DirOperationOptions>,
77107
},
78108
/// The remove file API.
79109
RemoveFile {
80-
path: PathBuf,
110+
path: SafePathBuf,
81111
options: Option<FileOperationOptions>,
82112
},
83113
/// The rename file API.
84114
#[serde(rename_all = "camelCase")]
85115
RenameFile {
86-
old_path: PathBuf,
87-
new_path: PathBuf,
116+
old_path: SafePathBuf,
117+
new_path: SafePathBuf,
88118
options: Option<FileOperationOptions>,
89119
},
90120
}
@@ -93,7 +123,7 @@ impl Cmd {
93123
#[module_command_handler(fs_read_file, "fs > readFile")]
94124
fn read_file<R: Runtime>(
95125
context: InvokeContext<R>,
96-
path: PathBuf,
126+
path: SafePathBuf,
97127
options: Option<FileOperationOptions>,
98128
) -> crate::Result<Vec<u8>> {
99129
file::read_binary(resolve_path(
@@ -109,7 +139,7 @@ impl Cmd {
109139
#[module_command_handler(fs_write_file, "fs > writeFile")]
110140
fn write_file<R: Runtime>(
111141
context: InvokeContext<R>,
112-
path: PathBuf,
142+
path: SafePathBuf,
113143
contents: Vec<u8>,
114144
options: Option<FileOperationOptions>,
115145
) -> crate::Result<()> {
@@ -127,7 +157,7 @@ impl Cmd {
127157
#[module_command_handler(fs_read_dir, "fs > readDir")]
128158
fn read_dir<R: Runtime>(
129159
context: InvokeContext<R>,
130-
path: PathBuf,
160+
path: SafePathBuf,
131161
options: Option<DirOperationOptions>,
132162
) -> crate::Result<Vec<dir::DiskEntry>> {
133163
let (recursive, dir) = if let Some(options_value) = options {
@@ -151,8 +181,8 @@ impl Cmd {
151181
#[module_command_handler(fs_copy_file, "fs > copyFile")]
152182
fn copy_file<R: Runtime>(
153183
context: InvokeContext<R>,
154-
source: PathBuf,
155-
destination: PathBuf,
184+
source: SafePathBuf,
185+
destination: SafePathBuf,
156186
options: Option<FileOperationOptions>,
157187
) -> crate::Result<()> {
158188
let (src, dest) = match options.and_then(|o| o.dir) {
@@ -181,7 +211,7 @@ impl Cmd {
181211
#[module_command_handler(fs_create_dir, "fs > createDir")]
182212
fn create_dir<R: Runtime>(
183213
context: InvokeContext<R>,
184-
path: PathBuf,
214+
path: SafePathBuf,
185215
options: Option<DirOperationOptions>,
186216
) -> crate::Result<()> {
187217
let (recursive, dir) = if let Some(options_value) = options {
@@ -208,7 +238,7 @@ impl Cmd {
208238
#[module_command_handler(fs_remove_dir, "fs > removeDir")]
209239
fn remove_dir<R: Runtime>(
210240
context: InvokeContext<R>,
211-
path: PathBuf,
241+
path: SafePathBuf,
212242
options: Option<DirOperationOptions>,
213243
) -> crate::Result<()> {
214244
let (recursive, dir) = if let Some(options_value) = options {
@@ -235,7 +265,7 @@ impl Cmd {
235265
#[module_command_handler(fs_remove_file, "fs > removeFile")]
236266
fn remove_file<R: Runtime>(
237267
context: InvokeContext<R>,
238-
path: PathBuf,
268+
path: SafePathBuf,
239269
options: Option<FileOperationOptions>,
240270
) -> crate::Result<()> {
241271
let resolved_path = resolve_path(
@@ -252,8 +282,8 @@ impl Cmd {
252282
#[module_command_handler(fs_rename_file, "fs > renameFile")]
253283
fn rename_file<R: Runtime>(
254284
context: InvokeContext<R>,
255-
old_path: PathBuf,
256-
new_path: PathBuf,
285+
old_path: SafePathBuf,
286+
new_path: SafePathBuf,
257287
options: Option<FileOperationOptions>,
258288
) -> crate::Result<()> {
259289
let (old, new) = match options.and_then(|o| o.dir) {
@@ -280,18 +310,18 @@ impl Cmd {
280310
}
281311

282312
#[allow(dead_code)]
283-
fn resolve_path<R: Runtime, P: AsRef<Path>>(
313+
fn resolve_path<R: Runtime>(
284314
config: &Config,
285315
package_info: &PackageInfo,
286316
window: &Window<R>,
287-
path: P,
317+
path: SafePathBuf,
288318
dir: Option<BaseDirectory>,
289-
) -> crate::Result<PathBuf> {
319+
) -> crate::Result<SafePathBuf> {
290320
let env = window.state::<Env>().inner();
291321
match crate::api::path::resolve_path(config, package_info, env, path, dir) {
292322
Ok(path) => {
293323
if window.state::<Scopes>().fs.is_allowed(&path) {
294-
Ok(path)
324+
Ok(SafePathBuf(path))
295325
} else {
296326
Err(crate::Error::PathNotAllowed(path))
297327
}
@@ -302,7 +332,7 @@ fn resolve_path<R: Runtime, P: AsRef<Path>>(
302332

303333
#[cfg(test)]
304334
mod tests {
305-
use std::path::PathBuf;
335+
use std::path::SafePathBuf;
306336

307337
use super::{BaseDirectory, DirOperationOptions, FileOperationOptions};
308338
use quickcheck::{Arbitrary, Gen};
@@ -336,28 +366,32 @@ mod tests {
336366

337367
#[tauri_macros::module_command_test(fs_read_file, "fs > readFile")]
338368
#[quickcheck_macros::quickcheck]
339-
fn read_file(path: PathBuf, options: Option<FileOperationOptions>) {
369+
fn read_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
340370
let res = super::Cmd::read_text_file(crate::test::mock_invoke_context(), path, options);
341371
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
342372
}
343373

344374
#[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")]
345375
#[quickcheck_macros::quickcheck]
346-
fn write_file(path: PathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
376+
fn write_file(path: SafePathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
347377
let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options);
348378
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
349379
}
350380

351381
#[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")]
352382
#[quickcheck_macros::quickcheck]
353-
fn read_dir(path: PathBuf, options: Option<DirOperationOptions>) {
383+
fn read_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
354384
let res = super::Cmd::read_dir(crate::test::mock_invoke_context(), path, options);
355385
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
356386
}
357387

358388
#[tauri_macros::module_command_test(fs_copy_file, "fs > copyFile")]
359389
#[quickcheck_macros::quickcheck]
360-
fn copy_file(source: PathBuf, destination: PathBuf, options: Option<FileOperationOptions>) {
390+
fn copy_file(
391+
source: SafePathBuf,
392+
destination: SafePathBuf,
393+
options: Option<FileOperationOptions>,
394+
) {
361395
let res = super::Cmd::copy_file(
362396
crate::test::mock_invoke_context(),
363397
source,
@@ -369,28 +403,32 @@ mod tests {
369403

370404
#[tauri_macros::module_command_test(fs_create_dir, "fs > createDir")]
371405
#[quickcheck_macros::quickcheck]
372-
fn create_dir(path: PathBuf, options: Option<DirOperationOptions>) {
406+
fn create_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
373407
let res = super::Cmd::create_dir(crate::test::mock_invoke_context(), path, options);
374408
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
375409
}
376410

377411
#[tauri_macros::module_command_test(fs_remove_dir, "fs > removeDir")]
378412
#[quickcheck_macros::quickcheck]
379-
fn remove_dir(path: PathBuf, options: Option<DirOperationOptions>) {
413+
fn remove_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
380414
let res = super::Cmd::remove_dir(crate::test::mock_invoke_context(), path, options);
381415
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
382416
}
383417

384418
#[tauri_macros::module_command_test(fs_remove_file, "fs > removeFile")]
385419
#[quickcheck_macros::quickcheck]
386-
fn remove_file(path: PathBuf, options: Option<FileOperationOptions>) {
420+
fn remove_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
387421
let res = super::Cmd::remove_file(crate::test::mock_invoke_context(), path, options);
388422
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
389423
}
390424

391425
#[tauri_macros::module_command_test(fs_rename_file, "fs > renameFile")]
392426
#[quickcheck_macros::quickcheck]
393-
fn rename_file(old_path: PathBuf, new_path: PathBuf, options: Option<FileOperationOptions>) {
427+
fn rename_file(
428+
old_path: SafePathBuf,
429+
new_path: SafePathBuf,
430+
options: Option<FileOperationOptions>,
431+
) {
394432
let res = super::Cmd::rename_file(
395433
crate::test::mock_invoke_context(),
396434
old_path,

0 commit comments

Comments
 (0)