diff options
Diffstat (limited to 'src/file_upload.rs')
-rw-r--r-- | src/file_upload.rs | 138 |
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::*; |