Skip to content

Commit

Permalink
feat(core): expose SafePathBuf (#6713)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Apr 15, 2023
1 parent 09376af commit 22a7633
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changes/safepathbuf-refactor.md
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Expose `SafePathBuf` type in `tauri::path`.
62 changes: 1 addition & 61 deletions core/tauri/src/api/file.rs
Expand Up @@ -8,59 +8,12 @@
mod extract;
mod file_move;

use std::{
fs,
path::{Display, Path},
};
use std::{fs, path::Path};

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

use serde::{de::Error as DeError, Deserialize, Deserializer};

#[derive(Clone, Debug)]
pub(crate) struct SafePathBuf(std::path::PathBuf);

impl SafePathBuf {
pub fn new(path: std::path::PathBuf) -> Result<Self, &'static str> {
if path
.components()
.any(|x| matches!(x, std::path::Component::ParentDir))
{
Err("cannot traverse directory, rewrite the path without the use of `../`")
} else {
Ok(Self(path))
}
}

#[allow(dead_code)]
pub unsafe fn new_unchecked(path: std::path::PathBuf) -> Self {
Self(path)
}

#[allow(dead_code)]
pub fn display(&self) -> Display<'_> {
self.0.display()
}
}

impl AsRef<Path> for SafePathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}

impl<'de> Deserialize<'de> for SafePathBuf {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let path = std::path::PathBuf::deserialize(deserializer)?;
SafePathBuf::new(path).map_err(DeError::custom)
}
}

/// Reads the entire contents of a file into a string.
pub fn read_string<P: AsRef<Path>>(file: P) -> crate::api::Result<String> {
fs::read_to_string(file).map_err(Into::into)
Expand All @@ -76,19 +29,6 @@ mod test {
use super::*;
#[cfg(not(windows))]
use crate::api::Error;
use quickcheck::{Arbitrary, Gen};

use std::path::PathBuf;

impl Arbitrary for super::SafePathBuf {
fn arbitrary(g: &mut Gen) -> Self {
Self(PathBuf::arbitrary(g))
}

fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
Box::new(self.0.shrink().map(SafePathBuf))
}
}

#[test]
fn check_read_string() {
Expand Down
7 changes: 2 additions & 5 deletions core/tauri/src/endpoints/file_system.rs
Expand Up @@ -5,11 +5,8 @@
#![allow(unused_imports)]

use crate::{
api::{
dir,
file::{self, SafePathBuf},
},
path::BaseDirectory,
api::{dir, file},
path::{BaseDirectory, SafePathBuf},
scope::Scopes,
Config, Env, Manager, PackageInfo, Runtime, Window,
};
Expand Down
4 changes: 1 addition & 3 deletions core/tauri/src/endpoints/http.rs
Expand Up @@ -102,9 +102,7 @@ impl Cmd {
..
} = value
{
if crate::api::file::SafePathBuf::new(path.clone()).is_err()
|| !scopes.fs.is_allowed(path)
{
if crate::path::SafePathBuf::new(path.clone()).is_err() || !scopes.fs.is_allowed(path) {
return Err(crate::Error::PathNotAllowed(path.clone()).into_anyhow());
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/tauri/src/manager.rs
Expand Up @@ -513,7 +513,7 @@ impl<R: Runtime> WindowManager<R> {

#[cfg(protocol_asset)]
if !registered_scheme_protocols.contains(&"asset".into()) {
use crate::api::file::SafePathBuf;
use crate::path::SafePathBuf;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use url::Position;
let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
Expand Down
64 changes: 63 additions & 1 deletion core/tauri/src/path/mod.rs
Expand Up @@ -4,14 +4,15 @@

use std::{
env::temp_dir,
path::{Component, Path, PathBuf},
path::{Component, Display, Path, PathBuf},
};

use crate::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};

use serde::{de::Error as DeError, Deserialize, Deserializer};
use serde_repr::{Deserialize_repr, Serialize_repr};

#[cfg(path_all)]
Expand All @@ -29,6 +30,49 @@ pub(crate) use android::PathResolver;
#[cfg(not(target_os = "android"))]
pub(crate) use desktop::PathResolver;

/// A wrapper for [`PathBuf`] that prevents path traversal.
#[derive(Clone, Debug)]
pub struct SafePathBuf(PathBuf);

impl SafePathBuf {
/// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe.
pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
if path.components().any(|x| matches!(x, Component::ParentDir)) {
Err("cannot traverse directory, rewrite the path without the use of `../`")
} else {
Ok(Self(path))
}
}

#[allow(dead_code)]
pub(crate) unsafe fn new_unchecked(path: PathBuf) -> Self {
Self(path)
}

/// Returns an object that implements [`std::fmt::Display`] for safely printing paths.
///
/// See [`PathBuf#method.display`] for more information.
pub fn display(&self) -> Display<'_> {
self.0.display()
}
}

impl AsRef<Path> for SafePathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}

impl<'de> Deserialize<'de> for SafePathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
SafePathBuf::new(path).map_err(DeError::custom)
}
}

/// A base directory to be used in [`resolve_directory`].
///
/// The base directory is the optional root of a file system operation.
Expand Down Expand Up @@ -332,3 +376,21 @@ pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
})
.build()
}

#[cfg(test)]
mod test {
use super::SafePathBuf;
use quickcheck::{Arbitrary, Gen};

use std::path::PathBuf;

impl Arbitrary for SafePathBuf {
fn arbitrary(g: &mut Gen) -> Self {
Self(PathBuf::arbitrary(g))
}

fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
Box::new(self.0.shrink().map(SafePathBuf))
}
}
}

0 comments on commit 22a7633

Please sign in to comment.