flawopen.com/Path Traversal/Rust
Rust guarantees memory safety, but not filesystem logic safety. If you join user input to a path without checking, an attacker can still step out of your app directory and read /etc/shadow.
Calling base_dir.join(user_input) in Rust simply pushes path components. If user_input contains ../ or starts with an absolute root slash on Unix, it breaks out of the expected directory.
Several popular Rust web crates and CLI utilities have required security patches when user-supplied paths in file-serving endpoints allowed reading source files or sensitive server configurations.
CISA Cybersecurity Advisory & MITRE CVE repository.// VULNERABLE: PathBuf::join allows directory traversal
use std::fs;
use std::path::PathBuf;
fn read_user_file(filename: &str) -> Result<Vec<u8>, std::io::Error> {
let base_dir = PathBuf::from("/var/app/public/files");
// Attacker input: "../../etc/passwd"
let target = base_dir.join(filename);
// Reads arbitrary system files!
fs::read(target)
}
// HARDENED: Canonicalize path and assert directory prefix
use std::fs;
use std::path::{Path, PathBuf};
use std::io::{Error, ErrorKind};
fn read_user_file(filename: &str) -> Result<Vec<u8>, Error> {
let base_dir = Path::new("/var/app/public/files").canonicalize()?;
// 1. Join and canonicalize target path (resolves .. and symlinks)
let target = base_dir.join(filename);
let resolved = target.canonicalize()?;
// 2. Strict boundary check: resolved must start with base_dir
if !resolved.starts_with(&base_dir) {
return Err(Error::new(ErrorKind::PermissionDenied, "Path traversal attempt detected"));
}
if !resolved.is_file() {
return Err(Error::new(ErrorKind::NotFound, "File not found"));
}
fs::read(resolved)
}
Rust's canonicalize() invokes the OS realpath syscall, resolving all traversal dots and symlinks. resolved.starts_with(&base_dir) verifies that the path belongs to the directory subtree.
canonicalize() returns io::ErrorKind::NotFound if the file does not exist yet. When creating new files, canonicalize the parent directory first.
The borrow checker enforces memory safety (no use-after-free, no data races), but cannot prevent semantic path traversal bugs in filesystem operations.
cargo clippy -- -D clippy::all
cargo audit
resolved.starts_with(&base_dir)resolved.is_file() before readingIn Rust, Path::join with an absolute path replaces the original path on Unix, exactly like Python. Canonicalization prevents this.