FlawOpen

flawopen.com/Path Traversal/Python os.path.join()

Does Python os.path.join() prevent path traversal?

CWE-22: Path TraversalReference page
Short answer

No. Think of the base directory as a room in a building: .. walks out through parent rooms, while an absolute path is a new street address that makes os.path.join() discard the room you started in. Resolve the final path and prove it remains under the allowed directory before opening it.

VULNERABLE
import os

base_dir = "/srv/uploads"
user_filename = "../../outside/demo.txt"
path = os.path.join(base_dir, user_filename)
# Relative traversal can leave the base.

path = os.path.join(base_dir, "/outside/demo.txt")
# An absolute segment discards base_dir.
SECURE — resolve and check containment
import os

def resolve_under_base(base_dir, user_filename):
    if not isinstance(user_filename, str) or not user_filename:
        raise ValueError("Invalid filename")

    base = os.path.realpath(base_dir)
    candidate = os.path.realpath(
        os.path.join(base, user_filename)
    )

    try:
        inside = os.path.commonpath((base, candidate)) == base
    except ValueError:  # e.g. different Windows drives
        inside = False

    if not inside or candidate == base:
        raise ValueError("Invalid filename")
    return candidate

Two path-join behaviors to account for

.. components can move a relative path above its intended directory. Separately, Python documents that an absolute segment causes os.path.join() to discard earlier segments; on Windows, drive and rooted-path rules differ from POSIX. pathlib.Path(base) / user_input has the same absolute-segment behavior. Do not treat joining or normalization as an authorization check.

Why commonpath() is safer than string prefixes

Resolve both the trusted base and candidate before comparing them. os.path.commonpath() compares path components rather than raw character prefixes and raises ValueError for incompatible roots or drives; reject those cases. realpath() resolves symlinks that exist when checked, but this check does not prevent a concurrent symlink change between validation and file open. If an attacker can modify the directory tree, use platform-appropriate descriptor-based access and no-follow controls.

FAQ

Does pathlib behave the same way?

Yes. The / operator on Path discards earlier components when the next path is absolute. Resolve the result and verify it is within the permitted base.

Does realpath() make the check race-safe?

No. It resolves symlinks present at validation time. Another process with write access can change a link before the later open. Restrict writes to the tree or use descriptor-relative operations with no-follow behavior where the platform supports them.

References

See also Path Traversal (CWE-22): How It Works and How to Prevent It in Every Language — flawopen.com What Is a Zip Slip Vulnerability? Node.js path.join() Does Not Prevent Traversal PHP include() and User-Controlled Paths