From 22a763381622407d58ae72aa24c0afff00b40e04 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 15 Apr 2023 11:44:05 -0700 Subject: [PATCH] feat(core): expose `SafePathBuf` (#6713) --- .changes/safepathbuf-refactor.md | 5 ++ core/tauri/src/api/file.rs | 62 +----------------------- core/tauri/src/endpoints/file_system.rs | 7 +-- core/tauri/src/endpoints/http.rs | 4 +- core/tauri/src/manager.rs | 2 +- core/tauri/src/path/mod.rs | 64 ++++++++++++++++++++++++- 6 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 .changes/safepathbuf-refactor.md diff --git a/.changes/safepathbuf-refactor.md b/.changes/safepathbuf-refactor.md new file mode 100644 index 00000000000..f80182d8ceb --- /dev/null +++ b/.changes/safepathbuf-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Expose `SafePathBuf` type in `tauri::path`. diff --git a/core/tauri/src/api/file.rs b/core/tauri/src/api/file.rs index 9e19456fb7d..4459dab3b61 100644 --- a/core/tauri/src/api/file.rs +++ b/core/tauri/src/api/file.rs @@ -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 { - 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 for SafePathBuf { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl<'de> Deserialize<'de> for SafePathBuf { - fn deserialize(deserializer: D) -> Result - 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>(file: P) -> crate::api::Result { fs::read_to_string(file).map_err(Into::into) @@ -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> { - Box::new(self.0.shrink().map(SafePathBuf)) - } - } #[test] fn check_read_string() { diff --git a/core/tauri/src/endpoints/file_system.rs b/core/tauri/src/endpoints/file_system.rs index 0c45b79a7f0..1a44789dced 100644 --- a/core/tauri/src/endpoints/file_system.rs +++ b/core/tauri/src/endpoints/file_system.rs @@ -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, }; diff --git a/core/tauri/src/endpoints/http.rs b/core/tauri/src/endpoints/http.rs index 4f6951791ea..50df3f2a0b0 100644 --- a/core/tauri/src/endpoints/http.rs +++ b/core/tauri/src/endpoints/http.rs @@ -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()); } } diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 875bc486178..37e08aaa95c 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -513,7 +513,7 @@ impl WindowManager { #[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::().asset_protocol.clone(); diff --git a/core/tauri/src/path/mod.rs b/core/tauri/src/path/mod.rs index 06310d19750..57b93ddb484 100644 --- a/core/tauri/src/path/mod.rs +++ b/core/tauri/src/path/mod.rs @@ -4,7 +4,7 @@ use std::{ env::temp_dir, - path::{Component, Path, PathBuf}, + path::{Component, Display, Path, PathBuf}, }; use crate::{ @@ -12,6 +12,7 @@ use crate::{ Manager, Runtime, }; +use serde::{de::Error as DeError, Deserialize, Deserializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(path_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 { + 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 for SafePathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl<'de> Deserialize<'de> for SafePathBuf { + fn deserialize(deserializer: D) -> std::result::Result + 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. @@ -332,3 +376,21 @@ pub(crate) fn init() -> TauriPlugin { }) .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> { + Box::new(self.0.shrink().map(SafePathBuf)) + } + } +}