aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAli MJ Al-Nasrawy <alimjalnasrawy@gmail.com>2021-03-23 19:42:28 +0000
committerAli MJ Al-Nasrawy <alimjalnasrawy@gmail.com>2021-09-01 11:02:19 +0000
commit699e17c7de0b40c1e5f7e4171683710378e4af58 (patch)
treeb68d53e1121091a3b0d6b2247a2d6a68d422f91e /src
parentAdd CHANGELOG entry for printing QR codes on terminal (diff)
downloadminiserve-699e17c7de0b40c1e5f7e4171683710378e4af58.tar.gz
miniserve-699e17c7de0b40c1e5f7e4171683710378e4af58.zip
file_upload.rs: sanitize path input
Signed-off-by: Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/file_upload.rs52
1 files changed, 42 insertions, 10 deletions
diff --git a/src/file_upload.rs b/src/file_upload.rs
index 5f9738c..2319b6a 100644
--- a/src/file_upload.rs
+++ b/src/file_upload.rs
@@ -2,7 +2,7 @@ use actix_web::{http::header, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use std::{
io::Write,
- path::{Component, PathBuf},
+ path::{Component, Path, PathBuf},
};
use crate::errors::ContextualError;
@@ -37,7 +37,7 @@ async fn save_file(
/// Create new future to handle file as multipart data.
async fn handle_multipart(
field: actix_multipart::Field,
- file_path: PathBuf,
+ path: PathBuf,
overwrite_files: bool,
) -> Result<u64, ContextualError> {
let filename = field
@@ -50,21 +50,25 @@ async fn handle_multipart(
)
})?;
- match std::fs::metadata(&file_path) {
+ let filename = sanitize_path(Path::new(&filename), false).ok_or_else(|| {
+ ContextualError::InvalidPathError("Invalid file name to upload".to_string())
+ })?;
+
+ match std::fs::metadata(&path) {
Err(_) => Err(ContextualError::InsufficientPermissionsError(
- file_path.display().to_string(),
+ path.display().to_string(),
)),
Ok(metadata) if !metadata.is_dir() => Err(ContextualError::InvalidPathError(format!(
"cannot upload file to {}, since it's not a directory",
- &file_path.display()
+ &path.display()
))),
Ok(metadata) if metadata.permissions().readonly() => Err(
- ContextualError::InsufficientPermissionsError(file_path.display().to_string()),
+ ContextualError::InsufficientPermissionsError(path.display().to_string()),
),
Ok(_) => Ok(()),
}?;
- save_file(field, file_path.join(filename), overwrite_files).await
+ save_file(field, path.join(filename), overwrite_files).await
}
/// Handle incoming request to upload file.
@@ -87,9 +91,9 @@ pub async fn upload_file(
let upload_path = query_params.path.as_ref().ok_or_else(|| {
ContextualError::InvalidHttpRequestError("Missing query parameter 'path'".to_string())
})?;
- let upload_path = upload_path
- .strip_prefix(Component::RootDir)
- .unwrap_or(upload_path);
+ let upload_path = sanitize_path(upload_path, conf.show_hidden).ok_or_else(|| {
+ ContextualError::InvalidPathError("Invalid value for 'path' parameter".to_string())
+ })?;
let app_root_dir = conf.path.canonicalize().map_err(|e| {
ContextualError::IoError("Failed to resolve path served by miniserve".to_string(), e)
@@ -97,6 +101,7 @@ pub async fn upload_file(
// If the target path is under the app root directory, save the file.
let target_dir = match app_root_dir.join(upload_path).canonicalize() {
+ Ok(path) if !conf.no_symlinks => Ok(path),
Ok(path) if path.starts_with(&app_root_dir) => Ok(path),
_ => Err(ContextualError::InvalidHttpRequestError(
"Invalid value for 'path' parameter".to_string(),
@@ -113,3 +118,30 @@ pub async fn upload_file(
.append_header((header::LOCATION, return_path))
.finish())
}
+
+/// Guarantee that the path is relative and cannot traverse back to parent directories
+/// and optionally prevent traversing hidden directories.
+fn sanitize_path(path: &Path, traverse_hidden: bool) -> Option<PathBuf> {
+ let mut buf = PathBuf::new();
+
+ for comp in path.components() {
+ match comp {
+ Component::Normal(name) => buf.push(name),
+ Component::ParentDir => {
+ buf.pop();
+ }
+ _ => (),
+ }
+ }
+
+ // Double-check that all components are Normal and check for hidden dirs
+ for comp in buf.components() {
+ match comp {
+ Component::Normal(_) if traverse_hidden => (),
+ Component::Normal(name) if !name.to_str()?.starts_with('.') => (),
+ _ => return None,
+ }
+ }
+
+ Some(buf)
+}