Path Normalization Guard
Normalize an untrusted relative path component by component and reject anything that would climb above the allowed root.
Code
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}"),
}
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.