aboutsummaryrefslogtreecommitdiffstats
path: root/src/file_upload.rs
diff options
context:
space:
mode:
authorSheepy <sheepy404@gmail.com>2022-06-26 00:02:02 +0000
committerGitHub <noreply@github.com>2022-06-26 00:02:02 +0000
commit5bcfa4ac832a9457ed32ff377febf6e284c5e1d5 (patch)
treef5c3eb3eab367f16d24535109955134f9c591e5c /src/file_upload.rs
parentBump clap_mangen from 0.1.8 to 0.1.9 (#826) (diff)
downloadminiserve-5bcfa4ac832a9457ed32ff377febf6e284c5e1d5.tar.gz
miniserve-5bcfa4ac832a9457ed32ff377febf6e284c5e1d5.zip
Create directory (#781)
* Add ability to make directory Frontend for making directories Fix potential security vulnerability (CWE-23) Add tests Update README.md Disallow using parent directories altogether Fix formatting Fix clippy warnings Address review comments Update README.md Change `making` to `creation` Co-authored-by: Sven-Hendrik Haase <svenstaro@gmail.com> Have make directory flag require file upload flag Address review comments * Disallow uploading files and making directories through symlinks when disabled * Add test * Clippy formatting changes * Add test doc comment
Diffstat (limited to '')
-rw-r--r--src/file_upload.rs138
1 files changed, 122 insertions, 16 deletions
diff --git a/src/file_upload.rs b/src/file_upload.rs
index 0d4b8a5..6643c68 100644
--- a/src/file_upload.rs
+++ b/src/file_upload.rs
@@ -39,20 +39,14 @@ async fn save_file(
/// Handles a single field in a multipart form
async fn handle_multipart(
- field: actix_multipart::Field,
+ mut field: actix_multipart::Field,
path: PathBuf,
overwrite_files: bool,
+ allow_mkdir: bool,
+ allow_hidden_paths: bool,
+ allow_symlinks: bool,
) -> Result<u64, ContextualError> {
- let filename = field.content_disposition().get_filename().ok_or_else(|| {
- ContextualError::ParseError(
- "HTTP header".to_string(),
- "Failed to retrieve the name of the file to upload".to_string(),
- )
- })?;
-
- let filename = sanitize_path(Path::new(&filename), false).ok_or_else(|| {
- ContextualError::InvalidPathError("Invalid file name to upload".to_string())
- })?;
+ let field_name = field.name().to_string();
match std::fs::metadata(&path) {
Err(_) => Err(ContextualError::InsufficientPermissionsError(
@@ -68,10 +62,88 @@ async fn handle_multipart(
Ok(_) => Ok(()),
}?;
- save_file(field, path.join(filename), overwrite_files).await
+ if field_name == "mkdir" {
+ if !allow_mkdir {
+ return Err(ContextualError::InsufficientPermissionsError(
+ path.display().to_string(),
+ ));
+ }
+
+ let mut user_given_path = PathBuf::new();
+ let mut absolute_path = path.clone();
+
+ // Get the path the user gave
+ let mkdir_path_bytes = field.try_next().await;
+ match mkdir_path_bytes {
+ Ok(Some(mkdir_path_bytes)) => {
+ let mkdir_path = std::str::from_utf8(&mkdir_path_bytes).map_err(|e| {
+ ContextualError::ParseError(
+ "Failed to parse 'mkdir' path".to_string(),
+ e.to_string(),
+ )
+ })?;
+ let mkdir_path = mkdir_path.replace('\\', "/");
+ absolute_path.push(&mkdir_path);
+ user_given_path.push(&mkdir_path);
+ }
+ _ => {
+ return Err(ContextualError::ParseError(
+ "Failed to parse 'mkdir' path".to_string(),
+ "".to_string(),
+ ))
+ }
+ };
+
+ // Disallow using `..` (parent) in mkdir path
+ if user_given_path
+ .components()
+ .any(|c| c == Component::ParentDir)
+ {
+ return Err(ContextualError::InvalidPathError(
+ "Cannot use '..' in mkdir path".to_string(),
+ ));
+ }
+ // Hidden paths check
+ sanitize_path(&user_given_path, allow_hidden_paths).ok_or_else(|| {
+ ContextualError::InvalidPathError("Cannot use hidden paths in mkdir path".to_string())
+ })?;
+
+ // Ensure there are no illegal symlinks
+ if !allow_symlinks && contains_symlink(&absolute_path) {
+ return Err(ContextualError::InsufficientPermissionsError(
+ user_given_path.display().to_string(),
+ ));
+ }
+
+ std::fs::create_dir_all(&absolute_path).map_err(|e| {
+ ContextualError::IoError(format!("Failed to create {}", user_given_path.display()), e)
+ })?;
+
+ return Ok(0);
+ }
+
+ let filename = field.content_disposition().get_filename().ok_or_else(|| {
+ ContextualError::ParseError(
+ "HTTP header".to_string(),
+ "Failed to retrieve the name of the file to upload".to_string(),
+ )
+ })?;
+
+ let filename_path = sanitize_path(Path::new(&filename), false).ok_or_else(|| {
+ ContextualError::InvalidPathError("Invalid file name to upload".to_string())
+ })?;
+
+ // Ensure there are no illegal symlinks in the file upload path
+ if !allow_symlinks && contains_symlink(&path) {
+ return Err(ContextualError::InsufficientPermissionsError(
+ filename.to_string(),
+ ));
+ }
+
+ save_file(field, path.join(filename_path), overwrite_files).await
}
-/// Handle incoming request to upload file.
+/// Handle incoming request to upload a file or create a directory.
/// Target file path is expected as path parameter in URI and is interpreted as relative from
/// server root directory. Any path which will go outside of this directory is considered
/// invalid.
@@ -99,8 +171,11 @@ pub async fn upload_file(
ContextualError::IoError("Failed to resolve path served by miniserve".to_string(), e)
})?;
- // If the target path is under the app root directory, save the file.
- let target_dir = match app_root_dir.join(upload_path).canonicalize() {
+ // Disallow the target path to go outside of the served directory
+ // The target directory shouldn't be canonicalized when it gets passed to
+ // handle_multipart so that it can check for symlinks if needed
+ let non_canonicalized_target_dir = app_root_dir.join(upload_path);
+ match non_canonicalized_target_dir.canonicalize() {
Ok(path) if !conf.no_symlinks => Ok(path),
Ok(path) if path.starts_with(&app_root_dir) => Ok(path),
_ => Err(ContextualError::InvalidHttpRequestError(
@@ -110,7 +185,16 @@ pub async fn upload_file(
actix_multipart::Multipart::new(req.headers(), payload)
.map_err(ContextualError::MultipartError)
- .and_then(|field| handle_multipart(field, target_dir.clone(), conf.overwrite_files))
+ .and_then(|field| {
+ handle_multipart(
+ field,
+ non_canonicalized_target_dir.clone(),
+ conf.overwrite_files,
+ conf.mkdir_enabled,
+ conf.show_hidden,
+ !conf.no_symlinks,
+ )
+ })
.try_collect::<Vec<u64>>()
.await?;
@@ -148,6 +232,28 @@ fn sanitize_path(path: &Path, traverse_hidden: bool) -> Option<PathBuf> {
Some(buf)
}
+/// Returns if a path goes through a symolic link
+fn contains_symlink(path: &PathBuf) -> bool {
+ let mut joined_path = PathBuf::new();
+ for path_slice in path {
+ joined_path = joined_path.join(path_slice);
+ if !joined_path.exists() {
+ // On Windows, `\\?\` won't exist even though it's the root
+ // So, we can't just return here
+ // But we don't need to check if it's a symlink since it won't be
+ continue;
+ }
+ if joined_path
+ .symlink_metadata()
+ .map(|m| m.file_type().is_symlink())
+ .unwrap_or(false)
+ {
+ return true;
+ }
+ }
+ false
+}
+
#[cfg(test)]
mod tests {
use super::*;