Pārlūkot izejas kodu

refactor(core): prevent path traversal [TRI-012] (#35)

Lucas Fernandes Nogueira 3 gadi atpakaļ
vecāks
revīzija
4d89f60d77

+ 5 - 0
.changes/prevent-path-traversal.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Prevent path traversal on the file system APIs.

+ 73 - 35
core/tauri/src/endpoints/file_system.rs

@@ -9,17 +9,47 @@ use crate::{
 };
 
 use super::InvokeContext;
-use serde::{Deserialize, Serialize};
+use serde::{
+  de::{Deserializer, Error as DeError},
+  Deserialize, Serialize,
+};
 use tauri_macros::{module_command_handler, CommandModule};
 
 use std::{
   fs,
   fs::File,
   io::Write,
-  path::{Path, PathBuf},
+  path::{Component, Path},
   sync::Arc,
 };
 
+pub struct SafePathBuf(std::path::PathBuf);
+
+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)?;
+    if path.components().any(|x| {
+      matches!(
+        x,
+        Component::ParentDir | Component::RootDir | Component::Prefix(_)
+      )
+    }) {
+      Err(DeError::custom("cannot traverse directory"))
+    } else {
+      Ok(SafePathBuf(path))
+    }
+  }
+}
+
 /// The options for the directory functions on the file system API.
 #[derive(Debug, Clone, Deserialize)]
 pub struct DirOperationOptions {
@@ -45,46 +75,46 @@ pub struct FileOperationOptions {
 pub enum Cmd {
   /// The read text file API.
   ReadFile {
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<FileOperationOptions>,
   },
   /// The write file API.
   WriteFile {
-    path: PathBuf,
+    path: SafePathBuf,
     contents: Vec<u8>,
     options: Option<FileOperationOptions>,
   },
   /// The read dir API.
   ReadDir {
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   },
   /// The copy file API.
   CopyFile {
-    source: PathBuf,
-    destination: PathBuf,
+    source: SafePathBuf,
+    destination: SafePathBuf,
     options: Option<FileOperationOptions>,
   },
   /// The create dir API.
   CreateDir {
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   },
   /// The remove dir API.
   RemoveDir {
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   },
   /// The remove file API.
   RemoveFile {
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<FileOperationOptions>,
   },
   /// The rename file API.
   #[serde(rename_all = "camelCase")]
   RenameFile {
-    old_path: PathBuf,
-    new_path: PathBuf,
+    old_path: SafePathBuf,
+    new_path: SafePathBuf,
     options: Option<FileOperationOptions>,
   },
 }
@@ -93,7 +123,7 @@ impl Cmd {
   #[module_command_handler(fs_read_file, "fs > readFile")]
   fn read_file<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<FileOperationOptions>,
   ) -> crate::Result<Vec<u8>> {
     file::read_binary(resolve_path(
@@ -109,7 +139,7 @@ impl Cmd {
   #[module_command_handler(fs_write_file, "fs > writeFile")]
   fn write_file<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     contents: Vec<u8>,
     options: Option<FileOperationOptions>,
   ) -> crate::Result<()> {
@@ -127,7 +157,7 @@ impl Cmd {
   #[module_command_handler(fs_read_dir, "fs > readDir")]
   fn read_dir<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   ) -> crate::Result<Vec<dir::DiskEntry>> {
     let (recursive, dir) = if let Some(options_value) = options {
@@ -151,8 +181,8 @@ impl Cmd {
   #[module_command_handler(fs_copy_file, "fs > copyFile")]
   fn copy_file<R: Runtime>(
     context: InvokeContext<R>,
-    source: PathBuf,
-    destination: PathBuf,
+    source: SafePathBuf,
+    destination: SafePathBuf,
     options: Option<FileOperationOptions>,
   ) -> crate::Result<()> {
     let (src, dest) = match options.and_then(|o| o.dir) {
@@ -181,7 +211,7 @@ impl Cmd {
   #[module_command_handler(fs_create_dir, "fs > createDir")]
   fn create_dir<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   ) -> crate::Result<()> {
     let (recursive, dir) = if let Some(options_value) = options {
@@ -208,7 +238,7 @@ impl Cmd {
   #[module_command_handler(fs_remove_dir, "fs > removeDir")]
   fn remove_dir<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<DirOperationOptions>,
   ) -> crate::Result<()> {
     let (recursive, dir) = if let Some(options_value) = options {
@@ -235,7 +265,7 @@ impl Cmd {
   #[module_command_handler(fs_remove_file, "fs > removeFile")]
   fn remove_file<R: Runtime>(
     context: InvokeContext<R>,
-    path: PathBuf,
+    path: SafePathBuf,
     options: Option<FileOperationOptions>,
   ) -> crate::Result<()> {
     let resolved_path = resolve_path(
@@ -252,8 +282,8 @@ impl Cmd {
   #[module_command_handler(fs_rename_file, "fs > renameFile")]
   fn rename_file<R: Runtime>(
     context: InvokeContext<R>,
-    old_path: PathBuf,
-    new_path: PathBuf,
+    old_path: SafePathBuf,
+    new_path: SafePathBuf,
     options: Option<FileOperationOptions>,
   ) -> crate::Result<()> {
     let (old, new) = match options.and_then(|o| o.dir) {
@@ -280,18 +310,18 @@ impl Cmd {
 }
 
 #[allow(dead_code)]
-fn resolve_path<R: Runtime, P: AsRef<Path>>(
+fn resolve_path<R: Runtime>(
   config: &Config,
   package_info: &PackageInfo,
   window: &Window<R>,
-  path: P,
+  path: SafePathBuf,
   dir: Option<BaseDirectory>,
-) -> crate::Result<PathBuf> {
+) -> crate::Result<SafePathBuf> {
   let env = window.state::<Env>().inner();
   match crate::api::path::resolve_path(config, package_info, env, path, dir) {
     Ok(path) => {
       if window.state::<Scopes>().fs.is_allowed(&path) {
-        Ok(path)
+        Ok(SafePathBuf(path))
       } else {
         Err(crate::Error::PathNotAllowed(path))
       }
@@ -302,7 +332,7 @@ fn resolve_path<R: Runtime, P: AsRef<Path>>(
 
 #[cfg(test)]
 mod tests {
-  use std::path::PathBuf;
+  use std::path::SafePathBuf;
 
   use super::{BaseDirectory, DirOperationOptions, FileOperationOptions};
   use quickcheck::{Arbitrary, Gen};
@@ -336,28 +366,32 @@ mod tests {
 
   #[tauri_macros::module_command_test(fs_read_file, "fs > readFile")]
   #[quickcheck_macros::quickcheck]
-  fn read_file(path: PathBuf, options: Option<FileOperationOptions>) {
+  fn read_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
     let res = super::Cmd::read_text_file(crate::test::mock_invoke_context(), path, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")]
   #[quickcheck_macros::quickcheck]
-  fn write_file(path: PathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
+  fn write_file(path: SafePathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
     let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")]
   #[quickcheck_macros::quickcheck]
-  fn read_dir(path: PathBuf, options: Option<DirOperationOptions>) {
+  fn read_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
     let res = super::Cmd::read_dir(crate::test::mock_invoke_context(), path, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_copy_file, "fs > copyFile")]
   #[quickcheck_macros::quickcheck]
-  fn copy_file(source: PathBuf, destination: PathBuf, options: Option<FileOperationOptions>) {
+  fn copy_file(
+    source: SafePathBuf,
+    destination: SafePathBuf,
+    options: Option<FileOperationOptions>,
+  ) {
     let res = super::Cmd::copy_file(
       crate::test::mock_invoke_context(),
       source,
@@ -369,28 +403,32 @@ mod tests {
 
   #[tauri_macros::module_command_test(fs_create_dir, "fs > createDir")]
   #[quickcheck_macros::quickcheck]
-  fn create_dir(path: PathBuf, options: Option<DirOperationOptions>) {
+  fn create_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
     let res = super::Cmd::create_dir(crate::test::mock_invoke_context(), path, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_remove_dir, "fs > removeDir")]
   #[quickcheck_macros::quickcheck]
-  fn remove_dir(path: PathBuf, options: Option<DirOperationOptions>) {
+  fn remove_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
     let res = super::Cmd::remove_dir(crate::test::mock_invoke_context(), path, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_remove_file, "fs > removeFile")]
   #[quickcheck_macros::quickcheck]
-  fn remove_file(path: PathBuf, options: Option<FileOperationOptions>) {
+  fn remove_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
     let res = super::Cmd::remove_file(crate::test::mock_invoke_context(), path, options);
     assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
   }
 
   #[tauri_macros::module_command_test(fs_rename_file, "fs > renameFile")]
   #[quickcheck_macros::quickcheck]
-  fn rename_file(old_path: PathBuf, new_path: PathBuf, options: Option<FileOperationOptions>) {
+  fn rename_file(
+    old_path: SafePathBuf,
+    new_path: SafePathBuf,
+    options: Option<FileOperationOptions>,
+  ) {
     let res = super::Cmd::rename_file(
       crate::test::mock_invoke_context(),
       old_path,