浏览代码

feat(core): expose `SafePathBuf` (#6713)

Lucas Fernandes Nogueira 2 年之前
父节点
当前提交
22a7633816

+ 5 - 0
.changes/safepathbuf-refactor.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Expose `SafePathBuf` type in `tauri::path`.

+ 1 - 61
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<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)
@@ -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() {

+ 2 - 5
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,
 };

+ 1 - 3
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());
             }
           }

+ 1 - 1
core/tauri/src/manager.rs

@@ -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();

+ 63 - 1
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<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.
@@ -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))
+    }
+  }
+}