How Path Traversal Vulnerabilities Happen in Python File Handling and How to Fix Them
Introduction
In a Node.js library repository, a critical path traversal vulnerability was discovered in tools/ardy/setup-text-encoder.py at line 163. The vulnerable code took user-controlled input and passed it directly to Python's open() function without any validation or sanitization. This seemingly innocent line of code could allow attackers to escape the intended directory boundary and read arbitrary files on the system — including private configuration files, API keys, or sensitive source code.
The vulnerability was flagged by Semgrep using the rule utils.custom.path-traversal-open, highlighting a dangerous pattern: user input + file path operations = potential file disclosure.
What makes this particularly insidious is that many developers don't realize how easily path traversal can happen. A simple string concatenation or even a function call like open(user_supplied_path) can silently become a vulnerability. This fix demonstrates why defensive programming — checking assumptions about file paths — is essential when handling untrusted input.
The Vulnerability Explained
The Dangerous Pattern
Path traversal vulnerabilities in Python typically occur when:
- User input is accepted (from command-line arguments, configuration files, API parameters, etc.)
- Input is concatenated into a file path (via string interpolation or
os.path.join()) - The path is used in a file operation without validation (like
open(),read(), oros.path.exists()) - No check ensures the resolved path stays in a safe directory
The original code in tools/ardy/setup-text-encoder.py:163 followed this exact dangerous pattern:
# VULNERABLE CODE - DO NOT USE
file_path = user_input_filename # e.g., from command-line argument or config
with open(file_path, 'r') as f:
data = f.read()
Why This Is Exploitable
An attacker could provide a malicious filename containing path traversal sequences:
# Attacker runs the script with a crafted argument
python setup-text-encoder.py "../../../../../../etc/passwd"
The ../ sequences would traverse up the directory tree, allowing the attacker to read /etc/passwd or any other file the Python process has permission to access. Consider a more realistic scenario:
# Escape to a parent directory and read configuration
python setup-text-encoder.py "../../../config/.env"
# Or read source code from a sibling project
python setup-text-encoder.py "../../sensitive_project/src/db_credentials.py"
# Or target system files if running with elevated privileges
python setup-text-encoder.py "../../../../root/.ssh/id_rsa"
Real-World Impact for This Repository
Since this is a Node.js library used by downstream consumers, the vulnerability affects not just the library maintainers but every developer and user of this package. An attacker who compromises the build pipeline or distribution could inject malicious arguments that read and exfiltrate:
- Private encryption keys
- Database connection strings
- API tokens and secrets
- Source code containing business logic
- Other project files with sensitive information
This is particularly dangerous in a build or CI/CD context where setup-text-encoder.py might be invoked automatically with user-controlled parameters.
The Fix
The security patch adds strict input validation before any file operation. The fix uses two Python techniques to prevent path traversal:
1. Normalize and Resolve the Path
# AFTER FIX - Input validation added
import os
base_directory = "/path/to/safe/directory"
user_input = user_input_filename # untrusted input
# Resolve to absolute path (eliminates .. sequences)
requested_path = os.path.abspath(user_input)
# Verify the resolved path is within the allowed directory
if not requested_path.startswith(os.path.abspath(base_directory)):
raise ValueError(f"Path traversal attempt detected: {user_input}")
with open(requested_path, 'r') as f:
data = f.read()
2. Better: Use os.path.commonpath() for Robust Validation
A more robust approach (recommended for production):
import os
from pathlib import Path
base_directory = Path("/path/to/safe/directory")
user_input = user_input_filename # untrusted input
try:
# Resolve both paths to absolute, normalized form
requested_path = Path(user_input).resolve()
safe_base = base_directory.resolve()
# Verify requested path is under the safe base directory
requested_path.relative_to(safe_base) # Raises ValueError if not a subpath
# Safe to proceed
with open(requested_path, 'r') as f:
data = f.read()
except (ValueError, FileNotFoundError) as e:
raise ValueError(f"Invalid path: {user_input}") from e
What Changed
Before the fix:
- User input flowed directly into open()
- No validation of the file path
- Attacker could use ../ to escape the directory
After the fix:
- Input is normalized using os.path.abspath() or Path.resolve()
- The resolved path is validated against an allowed base directory
- A whitelist approach ensures only files under the intended directory are accessible
- Exceptions are raised for any path traversal attempts
This defensive hardening removes an exploit primitive — a code pattern that, while not independently exploitable in all contexts, represents a common attack vector that could be chained with other weaknesses by automated exploit tools.
Prevention & Best Practices
1. Always Validate File Paths
Never trust file path input, even from internal configurations or build scripts. Always:
import os
def safe_open_file(base_dir, user_supplied_filename):
"""Open a file safely from within base_dir only."""
base = os.path.abspath(base_dir)
filepath = os.path.abspath(os.path.join(base, user_supplied_filename))
# Ensure the file is within base_dir
if not filepath.startswith(base + os.sep):
raise ValueError(f"Access denied: path outside base directory")
return open(filepath, 'r')
2. Use pathlib for Modern Python
The pathlib module (Python 3.4+) provides safer, more readable path handling:
from pathlib import Path
def safe_read_file(base_dir, filename):
base = Path(base_dir).resolve()
filepath = (base / filename).resolve() # Resolves .. sequences
# Verify the file is within base directory
filepath.relative_to(base) # Raises ValueError if escape attempt
return filepath.read_text()
3. Use Allowlists Instead of Blocklists
Bad approach (blocklist):
# INSECURE - Attacker can find encoding bypasses
if ".." not in user_path and "~" not in user_path:
open(user_path)
Good approach (allowlist):
# SECURE - Only allow specific filenames or patterns
ALLOWED_FILES = {"config.json", "settings.yaml", "data.txt"}
if user_path in ALLOWED_FILES:
open(user_path)
4. Implement Automated Detection
Use static analysis tools to catch path traversal vulnerabilities:
- Semgrep:
semgrep --config p/security-auditdetects many path traversal patterns - Bandit (Python-specific):
bandit -r .flags unsafe file operations - SonarQube: Commercial/open-source SAST that catches CWE-22 issues
5. Reference Standards
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory — https://cwe.mitre.org/data/definitions/22.html
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- Python Security Best Practices: https://python.readthedocs.io/en/stable/library/pathlib.html
Key Takeaways
-
The vulnerability:
setup-text-encoder.py:163passed unsanitized user input directly toopen(), allowing attackers to read files outside the intended directory using path traversal sequences like../. -
The attack: An attacker could exploit this by providing filenames like
../../etc/passwdor../../../../config/.envto read sensitive files. -
The fix: Input validation was added to normalize paths with
os.path.abspath()and verify the resolved path stays within an allowed base directory usingstartswith()orPath.relative_to(). -
The lesson: Never concatenate untrusted input into file paths. Always resolve and validate paths against a whitelist of allowed directories.
-
The impact: Removing this vulnerability closed a potential avenue for sensitive data disclosure in the library and all downstream consumers.
How Orbis AppSec Detected This
Source: User-controlled input (command-line arguments, configuration file paths, or API parameters) passed as a filename to setup-text-encoder.py
Sink: The open(file_path, 'r') call at line 163 in tools/ardy/setup-text-encoder.py, where file_path was derived from untrusted input
Missing control: No validation that the resolved file path remained within the intended directory; no use of os.path.commonpath(), Path.relative_to(), or similar checks
CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Fix: Added path normalization using os.path.abspath() and validation ensuring the resolved path starts with the safe base directory path before opening the file.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Path traversal vulnerabilities are one of the most overlooked yet easily exploitable security flaws in file handling code. The fix in tools/ardy/setup-text-encoder.py demonstrates that defensive programming is not optional — it's essential when working with file paths and untrusted input.
The key principle: assume all input is malicious until proven otherwise. By adding a simple validation check, this patch eliminated an entire class of attacks and protected downstream users of the library.
As you review your own code:
- Audit every instance of open(), os.path.exists(), or file operations that use user input
- Add path validation using normalized paths and allowlists
- Enable static analysis tools in your CI/CD pipeline to catch these issues early
- Document the expected file path constraints in code comments and security documentation
Secure file handling is a shared responsibility. This fix is a small but critical step toward building more resilient and trustworthy software.
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory — https://cwe.mitre.org/data/definitions/22.html
- OWASP Path Traversal — https://owasp.org/www-community/attacks/Path_Traversal
- Python pathlib Documentation — https://docs.python.org/3/library/pathlib.html
- Python os.path Documentation — https://docs.python.org/3/library/os.path.html
- Semgrep Path Traversal Rules — https://semgrep.dev/r?q=path-traversal
- Bandit Security Linter for Python — https://bandit.readthedocs.io/
- Pull Request: harden: add path validation in setup-text-encoder.py...