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

bug: hacking static folders #524

Merged
merged 3 commits into from
Dec 12, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 118 additions & 11 deletions resources/static-folder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
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> {
/// The folder to reach at runtime. Defaults to `static`
folder: &'a str,
}

pub enum Error {
AbsolutePath,
TransversedUp,
}

impl<'a> StaticFolder<'a> {
pub fn folder(mut self, folder: &'a str) -> Self {
self.folder = folder;
Expand All @@ -27,7 +38,23 @@ impl<'a> ResourceBuilder<PathBuf> for StaticFolder<'a> {
factory: &mut dyn Factory,
_runtime: &Runtime,
) -> Result<PathBuf, shuttle_service::Error> {
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())?;
Expand All @@ -36,9 +63,21 @@ impl<'a> ResourceBuilder<PathBuf> for StaticFolder<'a> {
}
}

impl From<Error> 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};
Expand All @@ -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]
Expand All @@ -80,23 +149,23 @@ mod tests {
}

fn get_build_path(&self) -> Result<std::path::PathBuf, shuttle_service::Error> {
Ok(self.build_path.path().to_owned())
Ok(self.build_path())
}

fn get_storage_path(&self) -> Result<std::path::PathBuf, shuttle_service::Error> {
Ok(self.storage_path.path().to_owned())
Ok(self.storage_path())
}
}

#[tokio::test]
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
Expand All @@ -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");
Expand All @@ -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();
}
}