Safe Path Join Against Directory Traversal

Resolve user-supplied relative paths under a fixed root, reject absolute or traversal inputs, and verify the final path stays inside.

PythonSecurity Boundaries

Code

snippet
from pathlib import Path


class UnsafePathError(ValueError):
    pass


def safe_join(root: str | Path, user_path: str) -> Path:
    if not isinstance(user_path, str) or not user_path.strip():
        raise UnsafePathError("path must be a non-empty string")
    if "\x00" in user_path:
        raise UnsafePathError("path must not contain NUL bytes")

    candidate = Path(user_path)
    if candidate.is_absolute() or candidate.drive:
        raise UnsafePathError("absolute paths are not allowed")

    base = Path(root).resolve()
    target = (base / candidate).resolve()
    try:
        target.relative_to(base)
    except ValueError as error:
        raise UnsafePathError(f"path escapes root: {user_path!r}") from error
    return target


def main() -> None:
    root = Path.cwd() / "public"
    for value in ["reports/2026/q1.csv", "../secrets.env", "/etc/passwd", "a/../../b"]:
        try:
            print(value, "->", safe_join(root, value))
        except UnsafePathError as error:
            print(value, "-> rejected:", error)


if __name__ == "__main__":
    main()