264. Path::join — One Absolute Path and Your Base Directory Is Gone
base.join(user_input) looks like it appends. But if the input starts with /, join silently throws your base away and returns the input alone.
The trap
Path::join (and PathBuf::push) have a documented surprise: if the argument is an absolute path, it replaces the whole path instead of appending.
| |
No panic, no warning — just a path that escaped your directory. If the joined segment comes from user input (an upload filename, a URL fragment, a config value), this is a path-traversal hole that .. filters won’t catch, because there’s no .. in sight.
The fix
Validate before you join: the input must be relative, and every component must be a normal segment — no .., no root, no prefix.
| |
The components() check rejects .. traversal in the same pass, so both escape routes are closed with one predicate.
One caveat
This blocks the lexical escapes, not symlinks — if the tree may contain attacker-created links, canonicalize and verify the result still starts with your base. And on Windows the same replacement happens with drive letters and prefixes (base.join("C:\\evil")), so the is_relative() check is doing real work on every platform.