Lack of data validation - Path Traversal
Need
Restriction of user-supplied file names to an allowed base directory
Context
• Usage of Rust 1.75+ for building application services
• Usage of the actix-web crate for handling HTTP requests
• Usage of std::fs and std::path for filesystem access
Description
1. Non compliant code
use actix_web::{get, web, HttpResponse};
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
struct ReadQuery {
filename: String,
}...The `read_report` handler below receives a `filename` query parameter and passes it straight to `fs::read_to_string`. Because the value is used as a complete path, the caller decides which file the server opens. A request to `/reports?filename=/etc/passwd` reads an absolute path, and `/reports?filename=../config/secrets.toml` walks out of the working directory with relative `..` segments. Both return the file contents in the response. Prefixing a base directory does not fix the problem on its own: `Path::new("reports").join("/etc/passwd")` returns `/etc/passwd`, because `Path::join` discards the base when the argument is absolute. The same flaw appears when extracting archives with the `zip` crate if `ZipFile::name()` is joined to the output directory, which lets an entry named `../../home/app/.ssh/authorized_keys` overwrite files outside it (Zip Slip).
2. Steps
• Never pass request data directly to `fs::read_to_string`, `File::open`, `File::create` or `actix_files::NamedFile::open`.
• Reduce user input to a single file name with `Path::file_name`, or map it to an identifier that the server translates into a path.
• Remember that `Path::join` replaces the base path when its argument is absolute; validate the value before joining it.
• Canonicalize the final path and confirm with `starts_with` that it stays inside the intended base directory.
• When extracting archives with the `zip` crate, build output paths from `ZipFile::enclosed_name()` and skip entries for which it returns `None`.
• Run the service with a filesystem user that can only read the directories it needs.
3. Secure code example
use actix_web::{get, web, HttpResponse};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
const BASE_DIR: &str = "/srv/app/reports";
#[derive(Deserialize)]...The corrected handler resolves the requested name through `resolve_report` before touching the filesystem. `Path::file_name` keeps only the last component of the value, so leading `/`, `..` segments and subdirectories are dropped and the name can no longer select a location. The result is joined to a fixed base directory and both paths are canonicalized, which resolves symbolic links. The final `starts_with` check rejects any file that still ends up outside the base directory, for example through a symlink planted inside it. When the name is empty or cannot be resolved, the handler answers `400 Bad Request` without revealing whether a file exists. For archive extraction, the equivalent control in the `zip` crate is `ZipFile::enclosed_name()`, which returns `None` for entries whose path would escape the output directory.
References
• 063. Lack of data validation - Path Traversal