Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rustapi-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository.workspace = true
homepage.workspace = true

[dependencies]
percent-encoding = "2.3"
# Async
tokio = { workspace = true, features = ["rt", "net", "time", "fs", "macros", "io-util"] }
futures-util = { workspace = true }
Expand Down
24 changes: 22 additions & 2 deletions crates/rustapi-core/src/static_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,13 @@ impl StaticFile {
relative_path: &str,
config: &StaticFileConfig,
) -> Result<Response, ApiError> {
// Sanitize path to prevent directory traversal
let clean_path = sanitize_path(relative_path);
// Percent-decode the path first
let decoded_path = percent_encoding::percent_decode_str(relative_path)
.decode_utf8()
.unwrap_or(std::borrow::Cow::Borrowed(relative_path));

// Sanitize path to prevent basic directory traversal
let clean_path = sanitize_path(&decoded_path);
let file_path = config.root.join(&clean_path);

// Check if it's a directory
Expand Down Expand Up @@ -282,6 +287,21 @@ impl StaticFile {

/// Serve a specific file
async fn serve_file(path: &Path, config: &StaticFileConfig) -> Result<Response, ApiError> {
// Security check: ensure the resolved path is within the root directory
let canonical_root = match tokio::fs::canonicalize(&config.root).await {
Ok(root) => root,
Err(_) => return Err(ApiError::internal("Static file root directory not found")),
};
Comment on lines +290 to +294

let canonical_file = match tokio::fs::canonicalize(path).await {
Ok(file) => file,
Err(_) => return Err(ApiError::not_found("File not found")),
};

Comment on lines +296 to +300
if !canonical_file.starts_with(&canonical_root) {
return Err(ApiError::not_found("File not found"));
}

// Check if file exists
let metadata = fs::metadata(path)
.await
Expand Down
53 changes: 53 additions & 0 deletions crates/rustapi-core/tests/static_files_security_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use rustapi_core::static_files::serve_dir;
use rustapi_core::static_files::StaticFile;
use std::fs::File;

#[tokio::test]
async fn test_directory_traversal_blocked() {
let config = serve_dir("/static", "./"); // root is crates/rustapi-core

// We try to access something outside the root
let relative_path = "../../etc/passwd";
Comment on lines +7 to +10
let res = StaticFile::serve(relative_path, &config).await;
assert!(res.is_err(), "Standard traversal should be blocked");

// Percent encoded payload
let relative_path_encoded = "..%2F..%2Fetc%2Fpasswd";
let res_encoded = StaticFile::serve(relative_path_encoded, &config).await;
assert!(res_encoded.is_err(), "Encoded traversal should be blocked");

// Double encoded
let relative_path_double = "%2e%2e%2f%2e%2e%2fetc%2fpasswd";
let res_double = StaticFile::serve(relative_path_double, &config).await;
assert!(
res_double.is_err(),
"Double encoded traversal should be blocked"
);
}

#[tokio::test]
async fn test_valid_file_served() {
let config = serve_dir("/static", "./");

// Valid file
let relative_path = "src/lib.rs";
let res = StaticFile::serve(relative_path, &config).await;
assert!(res.is_ok(), "Valid file should be served");
}

#[tokio::test]
async fn test_valid_file_with_spaces_served() {
let _ = std::fs::create_dir_all("./test_dir");
let _ = File::create("./test_dir/file with spaces.txt");

let config = serve_dir("/static", "./test_dir");
Comment on lines +40 to +43

let relative_path = "file%20with%20spaces.txt";
let res = StaticFile::serve(relative_path, &config).await;
assert!(
res.is_ok(),
"File with percent-encoded spaces should be served"
);

let _ = std::fs::remove_dir_all("./test_dir");
}
Loading