From 84250dadaf59a3c7cebcf045fa577cafa9131090 Mon Sep 17 00:00:00 2001 From: Pieter Date: Mon, 12 Dec 2022 11:11:02 +0200 Subject: [PATCH] bug: hacking static folders (#524) --- resources/static-folder/src/lib.rs | 129 ++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/resources/static-folder/src/lib.rs b/resources/static-folder/src/lib.rs index 435a6e9d7..259288dd9 100644 --- a/resources/static-folder/src/lib.rs +++ b/resources/static-folder/src/lib.rs @@ -1,6 +1,12 @@ use async_trait::async_trait; -use shuttle_service::{Factory, ResourceBuilder}; -use std::{fs::rename, path::PathBuf}; +use shuttle_service::{ + error::{CustomError, Error as ShuttleError}, + Factory, ResourceBuilder, +}; +use std::{ + fs::rename, + path::{Path, PathBuf}, +}; use tokio::runtime::Runtime; pub struct StaticFolder<'a> { @@ -8,6 +14,11 @@ pub struct StaticFolder<'a> { folder: &'a str, } +pub enum Error { + AbsolutePath, + TransversedUp, +} + impl<'a> StaticFolder<'a> { pub fn folder(mut self, folder: &'a str) -> Self { self.folder = folder; @@ -27,7 +38,23 @@ impl<'a> ResourceBuilder for StaticFolder<'a> { factory: &mut dyn Factory, _runtime: &Runtime, ) -> Result { + let folder = Path::new(self.folder); + + // Prevent users from users from reading anything outside of their crate's build folder + if folder.is_absolute() { + return Err(Error::AbsolutePath)?; + } + let input_dir = factory.get_build_path()?.join(self.folder); + + match input_dir.canonicalize() { + Ok(canonical_path) if canonical_path != input_dir => return Err(Error::TransversedUp)?, + Ok(_) => { + // The path did not change to outside the crate's build folder + } + Err(err) => return Err(err)?, + } + let output_dir = factory.get_storage_path()?.join(self.folder); rename(input_dir, output_dir.clone())?; @@ -36,9 +63,21 @@ impl<'a> ResourceBuilder for StaticFolder<'a> { } } +impl From for shuttle_service::Error { + fn from(error: Error) -> Self { + let msg = match error { + Error::AbsolutePath => "Cannot use an absolute path for a static folder", + Error::TransversedUp => "Cannot transverse out of crate for a static folder", + }; + + ShuttleError::Custom(CustomError::msg(msg)) + } +} + #[cfg(test)] mod tests { use std::fs::{self}; + use std::path::PathBuf; use async_trait::async_trait; use shuttle_service::{Factory, ResourceBuilder}; @@ -47,17 +86,47 @@ mod tests { use crate::StaticFolder; struct MockFactory { - build_path: TempDir, - storage_path: TempDir, + temp_dir: TempDir, } + // Will have this tree across all the tests + // . + // ├── build + // │   └── static + // │    └── note.txt + // ├── storage + // │   └── static + // │    └── note.txt + // └── escape + //    └── passwd impl MockFactory { fn new() -> Self { Self { - build_path: TempDir::new("build").unwrap(), - storage_path: TempDir::new("storage").unwrap(), + temp_dir: TempDir::new("static_folder").unwrap(), } } + + fn build_path(&self) -> PathBuf { + self.get_path("build") + } + + fn storage_path(&self) -> PathBuf { + self.get_path("storage") + } + + fn escape_path(&self) -> PathBuf { + self.get_path("escape") + } + + fn get_path(&self, folder: &str) -> PathBuf { + let path = self.temp_dir.path().join(folder); + + if !path.exists() { + fs::create_dir(&path).unwrap(); + } + + path + } } #[async_trait] @@ -80,11 +149,11 @@ mod tests { } fn get_build_path(&self) -> Result { - Ok(self.build_path.path().to_owned()) + Ok(self.build_path()) } fn get_storage_path(&self) -> Result { - Ok(self.storage_path.path().to_owned()) + Ok(self.storage_path()) } } @@ -92,11 +161,11 @@ mod tests { async fn copies_folder() { let mut factory = MockFactory::new(); - let input_file_path = factory.build_path.path().join("static").join("note.txt"); + let input_file_path = factory.build_path().join("static").join("note.txt"); fs::create_dir_all(input_file_path.parent().unwrap()).unwrap(); fs::write(input_file_path, "Hello, test!").unwrap(); - let expected_file = factory.storage_path.path().join("static").join("note.txt"); + let expected_file = factory.storage_path().join("static").join("note.txt"); assert!(!expected_file.exists(), "input file should not exist yet"); // Call plugin @@ -107,7 +176,7 @@ mod tests { assert_eq!( actual_folder, - factory.storage_path.path().join("static"), + factory.storage_path().join("static"), "expect path to the static folder" ); assert!(expected_file.exists(), "expected input file to be created"); @@ -119,4 +188,42 @@ mod tests { runtime.shutdown_background(); } + + #[tokio::test] + #[should_panic(expected = "Cannot use an absolute path for a static folder")] + async fn cannot_use_absolute_path() { + let mut factory = MockFactory::new(); + let static_folder = StaticFolder::new(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + + let _ = static_folder + .folder("/etc") + .build(&mut factory, &runtime) + .await + .unwrap(); + + runtime.shutdown_background(); + } + + #[tokio::test] + #[should_panic(expected = "Cannot transverse out of crate for a static folder")] + async fn cannot_transverse_up() { + let mut factory = MockFactory::new(); + + let password_file_path = factory.escape_path().join("passwd"); + fs::create_dir_all(password_file_path.parent().unwrap()).unwrap(); + fs::write(password_file_path, "qwerty").unwrap(); + + // Call plugin + let static_folder = StaticFolder::new(); + + let runtime = tokio::runtime::Runtime::new().unwrap(); + let _ = static_folder + .folder("../escape") + .build(&mut factory, &runtime) + .await + .unwrap(); + + runtime.shutdown_background(); + } }