Interactive Playground
Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.
pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-041 --project .About This Rule
Understanding the vulnerability and how it is detected
This rule detects path traversal vulnerabilities in Django applications where untrusted user input from HTTP request parameters flows into os.path.join() before being used in file system operations.
os.path.join() is commonly believed to be safe for constructing file paths because it handles path separator differences. However, os.path.join() has a critical security behavior: if any component is an absolute path (starts with /), it discards all previous components and uses the absolute path as the new base. This means os.path.join('/var/uploads', '/etc/passwd') returns '/etc/passwd'.
Additionally, os.path.join() does not strip or normalize '../' traversal sequences, so os.path.join('/var/uploads', '../../../etc/passwd') returns a path that resolves outside the intended directory. These behaviors make os.path.join() with user input a path traversal vulnerability unless the result is validated with os.path.realpath().
Security Implications
Potential attack scenarios if this vulnerability is exploited
Absolute Path Override via os.path.join() Semantics
os.path.join('/uploads', user_input) where user_input is '/etc/passwd' returns '/etc/passwd' -- the upload directory prefix is silently discarded. This is a common misunderstanding of os.path.join() safety. Attackers who know this behavior can supply absolute paths to bypass any attempt to restrict access to a subdirectory.
Relative Traversal via ../ Sequences
os.path.join('/uploads', '../../../etc/shadow') returns a path that, when passed to open(), resolves to /etc/shadow. Standard traversal payloads work unchanged through os.path.join() because the function makes no attempt to normalize or restrict component values.
Symlink-Based Escape Even After basename()
If symbolic links are present within the intended directory, a path constructed with os.path.join() may resolve outside it even when basename() was applied first. The realpath() check is essential to catch symlink-based traversal that escapes the intended directory boundary.
Write Access to Critical Files
If os.path.join() output feeds into open() in write mode, log rotation scripts, or file deletion operations, path traversal enables overwriting configuration files, log files, or application code. Writing to .py files in auto-reload servers causes code execution.
How to Fix
Recommended remediation steps
- 1After os.path.join(), always call os.path.realpath() to resolve symlinks and normalize the path, then verify the result starts with the intended base directory + os.sep.
- 2Apply os.path.basename() to user-provided filename components before passing them to os.path.join() to strip absolute path and traversal sequences.
- 3Use an explicit allowlist of permitted filenames or file IDs stored in the database rather than constructing paths from user input at all.
- 4Avoid accepting file paths as request parameters; instead, accept file IDs that map to paths stored securely in the database.
- 5When serving media files, use Django's storage API (default_storage.url()) rather than constructing raw filesystem paths.
Detection Scope
How Code Pathfinder analyzes your code for this vulnerability
This rule performs inter-procedural taint analysis with global scope. Sources include calls("request.GET.get"), calls("request.POST.get"), calls("request.GET.__getitem__"), calls("request.POST.__getitem__"), calls("request.body"), and calls("request.read"). The sink is calls("os.path.join") where any tainted value appears in the arguments (tracked via .tracks(0)), with the resulting path subsequently used in file operations. The rule also tracks the result of os.path.join() forward to file operation functions. Sanitizers include os.path.basename() applied before join plus os.path.realpath() with startswith verification. The rule follows taint across file and module boundaries.
Compliance & Standards
Industry frameworks and regulations that require detection of this vulnerability
References
External resources and documentation
Similar Rules
Explore related security rules for Python
Django SQL Injection via cursor.execute()
User input flows to cursor.execute() without parameterization, enabling SQL injection attacks.
Django SQL Injection via QuerySet.raw()
User input flows to QuerySet.raw() without parameterization, enabling SQL injection through Django's ORM raw query interface.
Django SQL Injection via QuerySet.extra()
User input flows to QuerySet.extra() without parameterization, enabling SQL injection through Django's legacy ORM extension interface.
Frequently Asked Questions
Common questions about Django Path Traversal via os.path.join()
New feature
Get these findings posted directly on your GitHub pull requests
The Django Path Traversal via os.path.join() rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.