Stop Directory Traversal with Python Path.resolve()
Directory traversal vulnerabilities pose a severe security risk by
allowing attackers to manipulate file paths using relative sequences
such as ../ to access unauthorized files across the host
system. In Python, the pathlib.Path.resolve() method serves
as an essential defense by eliminating relative segments, expanding
symbolic links, and returning a fully canonical absolute path. This
article explains how Path.resolve() processes path
structures, why naive path joins fail, and how to implement strict
boundary validation to completely neutralize path traversal
exploits.
The Mechanism of a Directory Traversal Exploit
Directory traversal (or path traversal) occurs when untrusted user
input is concatenated directly with a base storage path. For example, if
an application intends to serve files exclusively from
/var/www/uploads, an attacker might pass the string
../../etc/shadow.
A basic string concatenation or naive join operation creates
/var/www/uploads/../../etc/shadow. If passed directly to an
operating system file descriptor without resolution, the filesystem
navigates backward out of the intended directory tree, exposing
sensitive system data or allowing arbitrary file writes.
How
Path.resolve() Neutralizes Relative Paths
The resolve() method in Python's pathlib
module processes path tokens at the filesystem level to compute the
canonical, absolute path. It prevents directory traversal exploits
through two core behaviors:
- Path Normalization: It evaluates all relative
components (
.and..). A path like/var/www/uploads/../../etc/shadowis calculated and transformed into its actual representation:/etc/shadow. - Symlink Resolution: It resolves symbolic links to
their real disk locations. Attackers often attempt to circumvent path
filters by creating symlinks inside an allowed directory that point
outward to restricted directories.
Path.resolve()dereferences these links, exposing the true destination.
Implementing Secure Confinement
Calling Path.resolve() alone does not block an attack;
it reveals the true target of the user's input. The security guarantee
comes from pairing Path.resolve() with a boundary check to
verify that the canonical target resides within the canonical base
directory.
In Python 3.9 and later, the recommended approach utilizes
Path.is_relative_to():
from pathlib import Path
def get_safe_file_path(base_directory: Path, untrusted_filename: str) -> Path:
# 1. Resolve the trusted base directory to its canonical absolute path
resolved_base = base_directory.resolve()
# 2. Join the base and untrusted input, then fully resolve the result
resolved_target = (resolved_base / untrusted_filename).resolve()
# 3. Verify the target is strictly inside or equal to the base directory
if not resolved_target.is_relative_to(resolved_base):
raise PermissionError("Directory traversal detected: access denied.")
return resolved_targetFor environments running Python versions prior to 3.9, the same
validation can be enforced using Path.relative_to() inside
a try/except block:
try:
resolved_target.relative_to(resolved_base)
except ValueError:
raise PermissionError("Directory traversal detected: access denied.")If resolved_target escapes resolved_base,
relative_to() raises a ValueError, immediately
catching any traversal attempt.
Handling New or Non-Existent Files
When preparing paths for file writing, the target file may not yet
exist on disk. By default, calling resolve(strict=False)
enables Path.resolve() to resolve existing parent
components while canonicalizing the remaining non-existent segments
without throwing a FileNotFoundError. This ensures write
boundaries are validated before creating new files.