#175 Jun 1, 2026

175. PathBuf::push — When an Absolute Argument Wipes Your Base Path

base.push(user_input) looks like string concatenation for paths. It isn’t — if user_input is absolute, the original base is gone.

The footgun

PathBuf::push reads almost like += for paths. Most of the time, it behaves that way:

1
2
3
4
5
6
use std::path::PathBuf;

let mut p = PathBuf::from("/home/alice");
p.push("docs");
p.push("notes.txt");
assert_eq!(p, PathBuf::from("/home/alice/docs/notes.txt"));

But the moment the pushed component is absolute, push throws the existing buffer away and starts over:

1
2
3
4
5
use std::path::PathBuf;

let mut p = PathBuf::from("/home/alice");
p.push("/etc/passwd");
assert_eq!(p, PathBuf::from("/etc/passwd"));

That’s not a bug. The docs spell it out: “if path is absolute, it replaces the current path.” It mirrors how cd /etc/passwd works in a shell. The catch is that when one half of push is user input, the cd-like behavior turns into a path-traversal vector.

Why this bites

The most common shape of the bug:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::path::PathBuf;

fn user_file(home: &str, requested: &str) -> PathBuf {
    let mut p = PathBuf::from(home);
    p.push(requested);
    p
}

assert_eq!(
    user_file("/srv/users/alice", "avatar.png"),
    PathBuf::from("/srv/users/alice/avatar.png"),
);

// An absolute `requested` silently escapes the sandbox.
assert_eq!(
    user_file("/srv/users/alice", "/etc/passwd"),
    PathBuf::from("/etc/passwd"),
);

No panic, no error, no warning. The function just hands back a path that points somewhere else entirely.

The fix

Reject absolute components before joining. Path::is_absolute and Path::has_root are the two checks you need:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::path::{Path, PathBuf};

fn safe_join(base: &Path, requested: &str) -> Option<PathBuf> {
    let segment = Path::new(requested);
    if segment.is_absolute() || segment.has_root() {
        return None;
    }
    Some(base.join(segment))
}

assert_eq!(
    safe_join(Path::new("/srv/users/alice"), "avatar.png"),
    Some(PathBuf::from("/srv/users/alice/avatar.png")),
);
assert_eq!(safe_join(Path::new("/srv/users/alice"), "/etc/passwd"), None);

has_root matters on Windows too — \windows\system32 has a root but no drive prefix, and push will replace the non-prefix portion of your buffer with it. is_absolute alone misses that case on Windows.

For full sandbox enforcement you also want to canonicalize and check the result is still under base.. components can still escape — but stopping the absolute-path case is the cheap first line of defence.

Takeaway

PathBuf::push is not string concatenation. Treat any component you didn’t write yourself as suspect and gate it through is_absolute / has_root before letting it near your buffer.

← Previous 174. iter::repeat_with — Build N Fresh Things When vec![] Won't Clone