Skip to content
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

feat(core): expose SafePathBuf #6713

Merged
merged 3 commits into from Apr 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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))
}
}
}