Skip to content

Commit 34a402f

Browse files
authored
fix(core): do not allow path traversal on the asset protocol (#3774)
1 parent 8661e3e commit 34a402f

File tree

5 files changed

+93
-52
lines changed

5 files changed

+93
-52
lines changed
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+
Do not allow path traversal on the asset protocol.

core/tauri/src/api/file.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,57 @@
88
mod extract;
99
mod file_move;
1010

11-
use std::{fs, path::Path};
11+
use std::{
12+
fs,
13+
path::{Display, Path},
14+
};
1215

1316
#[cfg(feature = "fs-extract-api")]
1417
pub use extract::*;
1518
pub use file_move::*;
1619

20+
use serde::{de::Error as DeError, Deserialize, Deserializer};
21+
22+
#[derive(Clone, Debug)]
23+
pub(crate) struct SafePathBuf(std::path::PathBuf);
24+
25+
impl SafePathBuf {
26+
pub fn new(path: std::path::PathBuf) -> Result<Self, &'static str> {
27+
if path
28+
.components()
29+
.any(|x| matches!(x, std::path::Component::ParentDir))
30+
{
31+
Err("cannot traverse directory, rewrite the path without the use of `../`")
32+
} else {
33+
Ok(Self(path))
34+
}
35+
}
36+
37+
pub unsafe fn new_unchecked(path: std::path::PathBuf) -> Self {
38+
Self(path)
39+
}
40+
41+
pub fn display(&self) -> Display<'_> {
42+
self.0.display()
43+
}
44+
}
45+
46+
impl AsRef<Path> for SafePathBuf {
47+
fn as_ref(&self) -> &Path {
48+
self.0.as_ref()
49+
}
50+
}
51+
52+
impl<'de> Deserialize<'de> for SafePathBuf {
53+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54+
where
55+
D: Deserializer<'de>,
56+
{
57+
let path = std::path::PathBuf::deserialize(deserializer)?;
58+
SafePathBuf::new(path).map_err(DeError::custom)
59+
}
60+
}
61+
1762
/// Reads the entire contents of a file into a string.
1863
pub fn read_string<P: AsRef<Path>>(file: P) -> crate::api::Result<String> {
1964
fs::read_to_string(file).map_err(Into::into)
@@ -28,6 +73,19 @@ pub fn read_binary<P: AsRef<Path>>(file: P) -> crate::api::Result<Vec<u8>> {
2873
mod test {
2974
use super::*;
3075
use crate::api::Error;
76+
use quickcheck::{Arbitrary, Gen};
77+
78+
use std::path::PathBuf;
79+
80+
impl Arbitrary for super::SafePathBuf {
81+
fn arbitrary(g: &mut Gen) -> Self {
82+
Self(PathBuf::arbitrary(g))
83+
}
84+
85+
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
86+
Box::new(self.0.shrink().map(SafePathBuf))
87+
}
88+
}
3189

3290
#[test]
3391
fn check_read_string() {

core/tauri/src/api/path.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ pub fn parse<P: AsRef<Path>>(
170170
}
171171
p.push(component);
172172
}
173-
println!("res {:?}", p);
174173

175174
Ok(p)
176175
}

core/tauri/src/endpoints/file_system.rs

Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
// SPDX-License-Identifier: MIT
44

55
use crate::{
6-
api::{dir, file, path::BaseDirectory},
6+
api::{
7+
dir,
8+
file::{self, SafePathBuf},
9+
path::BaseDirectory,
10+
},
711
scope::Scopes,
812
Config, Env, Manager, PackageInfo, Runtime, Window,
913
};
@@ -26,29 +30,6 @@ use std::{
2630
sync::Arc,
2731
};
2832

29-
#[derive(Clone, Debug)]
30-
pub struct SafePathBuf(std::path::PathBuf);
31-
32-
impl AsRef<Path> for SafePathBuf {
33-
fn as_ref(&self) -> &Path {
34-
self.0.as_ref()
35-
}
36-
}
37-
38-
impl<'de> Deserialize<'de> for SafePathBuf {
39-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40-
where
41-
D: Deserializer<'de>,
42-
{
43-
let path = std::path::PathBuf::deserialize(deserializer)?;
44-
if path.components().any(|x| matches!(x, Component::ParentDir)) {
45-
Err(DeError::custom("cannot traverse directory"))
46-
} else {
47-
Ok(SafePathBuf(path))
48-
}
49-
}
50-
}
51-
5233
/// The options for the directory functions on the file system API.
5334
#[derive(Debug, Clone, Deserialize)]
5435
pub struct DirOperationOptions {
@@ -71,7 +52,7 @@ pub struct FileOperationOptions {
7152
/// The API descriptor.
7253
#[derive(Deserialize, CommandModule)]
7354
#[serde(tag = "cmd", rename_all = "camelCase")]
74-
pub enum Cmd {
55+
pub(crate) enum Cmd {
7556
/// The read binary file API.
7657
ReadFile {
7758
path: SafePathBuf,
@@ -138,7 +119,7 @@ impl Cmd {
138119
options.and_then(|o| o.dir),
139120
)?;
140121
file::read_binary(&resolved_path)
141-
.with_context(|| format!("path: {}", resolved_path.0.display()))
122+
.with_context(|| format!("path: {}", resolved_path.display()))
142123
.map_err(Into::into)
143124
}
144125

@@ -156,7 +137,7 @@ impl Cmd {
156137
options.and_then(|o| o.dir),
157138
)?;
158139
file::read_string(&resolved_path)
159-
.with_context(|| format!("path: {}", resolved_path.0.display()))
140+
.with_context(|| format!("path: {}", resolved_path.display()))
160141
.map_err(Into::into)
161142
}
162143

@@ -175,7 +156,7 @@ impl Cmd {
175156
options.and_then(|o| o.dir),
176157
)?;
177158
File::create(&resolved_path)
178-
.with_context(|| format!("path: {}", resolved_path.0.display()))
159+
.with_context(|| format!("path: {}", resolved_path.display()))
179160
.map_err(Into::into)
180161
.and_then(|mut f| f.write_all(&contents).map_err(|err| err.into()))
181162
}
@@ -199,7 +180,7 @@ impl Cmd {
199180
dir,
200181
)?;
201182
dir::read_dir(&resolved_path, recursive)
202-
.with_context(|| format!("path: {}", resolved_path.0.display()))
183+
.with_context(|| format!("path: {}", resolved_path.display()))
203184
.map_err(Into::into)
204185
}
205186

@@ -230,7 +211,7 @@ impl Cmd {
230211
None => (source, destination),
231212
};
232213
fs::copy(src.clone(), dest.clone())
233-
.with_context(|| format!("source: {}, dest: {}", src.0.display(), dest.0.display()))?;
214+
.with_context(|| format!("source: {}, dest: {}", src.display(), dest.display()))?;
234215
Ok(())
235216
}
236217

@@ -254,10 +235,10 @@ impl Cmd {
254235
)?;
255236
if recursive {
256237
fs::create_dir_all(&resolved_path)
257-
.with_context(|| format!("path: {}", resolved_path.0.display()))?;
238+
.with_context(|| format!("path: {}", resolved_path.display()))?;
258239
} else {
259240
fs::create_dir(&resolved_path)
260-
.with_context(|| format!("path: {} (non recursive)", resolved_path.0.display()))?;
241+
.with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?;
261242
}
262243

263244
Ok(())
@@ -283,10 +264,10 @@ impl Cmd {
283264
)?;
284265
if recursive {
285266
fs::remove_dir_all(&resolved_path)
286-
.with_context(|| format!("path: {}", resolved_path.0.display()))?;
267+
.with_context(|| format!("path: {}", resolved_path.display()))?;
287268
} else {
288269
fs::remove_dir(&resolved_path)
289-
.with_context(|| format!("path: {} (non recursive)", resolved_path.0.display()))?;
270+
.with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?;
290271
}
291272

292273
Ok(())
@@ -306,7 +287,7 @@ impl Cmd {
306287
options.and_then(|o| o.dir),
307288
)?;
308289
fs::remove_file(&resolved_path)
309-
.with_context(|| format!("path: {}", resolved_path.0.display()))?;
290+
.with_context(|| format!("path: {}", resolved_path.display()))?;
310291
Ok(())
311292
}
312293

@@ -337,7 +318,7 @@ impl Cmd {
337318
None => (old_path, new_path),
338319
};
339320
fs::rename(&old, &new)
340-
.with_context(|| format!("old: {}, new: {}", old.0.display(), new.0.display()))
321+
.with_context(|| format!("old: {}, new: {}", old.display(), new.display()))
341322
.map_err(Into::into)
342323
}
343324
}
@@ -354,15 +335,18 @@ fn resolve_path<R: Runtime>(
354335
match crate::api::path::resolve_path(config, package_info, env, &path, dir) {
355336
Ok(path) => {
356337
if window.state::<Scopes>().fs.is_allowed(&path) {
357-
Ok(SafePathBuf(path))
338+
Ok(
339+
// safety: the path is resolved by Tauri so it is safe
340+
unsafe { SafePathBuf::new_unchecked(path) },
341+
)
358342
} else {
359343
Err(anyhow::anyhow!(
360344
crate::Error::PathNotAllowed(path).to_string()
361345
))
362346
}
363347
}
364348
Err(e) => super::Result::<SafePathBuf>::Err(e.into())
365-
.with_context(|| format!("path: {}, base dir: {:?}", path.0.display(), dir)),
349+
.with_context(|| format!("path: {}, base dir: {:?}", path.display(), dir)),
366350
}
367351
}
368352

@@ -372,18 +356,6 @@ mod tests {
372356

373357
use quickcheck::{Arbitrary, Gen};
374358

375-
use std::path::PathBuf;
376-
377-
impl Arbitrary for super::SafePathBuf {
378-
fn arbitrary(g: &mut Gen) -> Self {
379-
Self(PathBuf::arbitrary(g))
380-
}
381-
382-
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
383-
Box::new(self.0.shrink().map(SafePathBuf))
384-
}
385-
}
386-
387359
impl Arbitrary for BaseDirectory {
388360
fn arbitrary(g: &mut Gen) -> Self {
389361
if bool::arbitrary(g) {

core/tauri/src/manager.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ impl<R: Runtime> WindowManager<R> {
498498

499499
#[cfg(protocol_asset)]
500500
if !registered_scheme_protocols.contains(&"asset".into()) {
501+
use crate::api::file::SafePathBuf;
501502
use tokio::io::{AsyncReadExt, AsyncSeekExt};
502503
use url::Position;
503504
let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
@@ -512,6 +513,12 @@ impl<R: Runtime> WindowManager<R> {
512513
.decode_utf8_lossy()
513514
.to_string();
514515

516+
if let Err(e) = SafePathBuf::new(path.clone().into()) {
517+
#[cfg(debug_assertions)]
518+
eprintln!("asset protocol path \"{}\" is not valid: {}", path, e);
519+
return HttpResponseBuilder::new().status(403).body(Vec::new());
520+
}
521+
515522
if !asset_scope.is_allowed(&path) {
516523
#[cfg(debug_assertions)]
517524
eprintln!("asset protocol not configured to allow the path: {}", path);

0 commit comments

Comments
 (0)