Path Normalization Guard

Normalize an untrusted relative path component by component and reject anything that would climb above the allowed root.

RustSecurity Boundaries

Code

snippet
use std::path::{Component, Path, PathBuf};

fn normalize_join(root: &Path, untrusted: &str) -> Result<PathBuf, String> {
    let candidate = Path::new(untrusted);
    if candidate.is_absolute() {
        return Err(format!("rejected absolute path: {untrusted}"));
    }

    let mut depth = 0usize;
    let mut normalized = PathBuf::new();
    for component in candidate.components() {
        match component {
            Component::Normal(part) => {
                normalized.push(part);
                depth += 1;
            }
            Component::CurDir => {}
            Component::ParentDir => {
                if depth == 0 {
                    return Err(format!("path escapes root: {untrusted}"));
                }
                normalized.pop();
                depth -= 1;
            }
            _ => return Err(format!("unsupported path component in {untrusted}")),
        }
    }

    let joined = root.join(&normalized);
    if !joined.starts_with(root) {
        return Err(format!("resolved path escapes root: {untrusted}"));
    }
    Ok(joined)
}

fn main() {
    let root = Path::new("/srv/uploads");
    let candidates = [
        "reports/2026/q1.csv",
        "reports/../notes.txt",
        "../secrets.env",
        "/etc/passwd",
        "a/../../b",
    ];
    for candidate in candidates {
        match normalize_join(root, candidate) {
            Ok(path) => println!("ok   {candidate:<22} => {}", path.display()),
            Err(error) => println!("skip {candidate:<22} => {error}"),
        }
    }
}