flawopen.com/Path Traversal/Swift

Path Traversal in Swift

High Severity CWE-22
ELI5

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.

Key terms on this page
Path::canonicalize
Returns the canonical, absolute form of a path with all intermediate components and symlinks resolved.
Path::starts_with
Returns true if the path starts with the given prefix component-by-component.

What's happening

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.

Real-world impact

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 vs. fixed

VULNERABLE
// 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)
}
FIXED
// 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)
}

Why the fix works

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.

Gotchas

canonicalize requires file existence

canonicalize() returns io::ErrorKind::NotFound if the file does not exist yet. When creating new files, canonicalize the parent directory first.

Common misconceptions

"Rust's borrow checker prevents path traversal"

The borrow checker enforces memory safety (no use-after-free, no data races), but cannot prevent semantic path traversal bugs in filesystem operations.

How to check if you're affected

cargo clippy -- -D clippy::all cargo audit

Prevention checklist

FAQ

What if user input starts with a slash in Rust?

In Rust, Path::join with an absolute path replaces the original path on Unix, exactly like Python. Canonicalization prevents this.

References