Back to Blog
high SEVERITY7 min read

How Path Traversal Vulnerabilities Happen in Python File Handling and How to Fix Them

A path traversal vulnerability was discovered in `tools/ardy/setup-text-encoder.py` at line 163, where user-controlled input was passed directly to `open()` without validation. This flaw could allow attackers to read sensitive files outside the intended directory. The fix adds strict path validation to ensure only legitimate files are accessed.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 31, 2026•Reviewed August 31, 2026

Answer Summary

A path traversal vulnerability (CWE-22) in Python's `setup-text-encoder.py` allowed unsanitized user input to be used in file paths passed to `open()`, potentially exposing arbitrary files. The fix adds input validation that sanitizes the file path and verifies it stays within the intended directory boundary before opening the file.

Vulnerability at a Glance

cweCWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
fixSanitize input paths and validate they remain within the intended directory using `os.path.abspath()` and `os.path.commonpath()` checks
riskUnauthorized file disclosure; attackers can read sensitive configuration, source code, or private data
languagePython
root causeUser-controlled input concatenated directly into file paths without validation or normalization
vulnerabilityPath Traversal via Unvalidated File Path

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:

  1. User input is accepted (from command-line arguments, configuration files, API parameters, etc.)
  2. Input is concatenated into a file path (via string interpolation or os.path.join())
  3. The path is used in a file operation without validation (like open(), read(), or os.path.exists())
  4. 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-audit detects 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:163 passed unsanitized user input directly to open(), 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/passwd or ../../../../config/.env to 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 using startswith() or Path.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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #36

Related Articles

high

clean_path() Path Traversal: rel_path Escapes USERDATA Dir

A path traversal flaw in the `clean_path()` helper let a user-controlled `rel_path` value escape the intended USERDATA directory and reach arbitrary files on disk. The function joined path segments without checking the final result, so `../` sequences in route parameters or query strings could be used to read or write files outside the sandboxed storage area. The fix normalizes the path and verifies it still resolves inside the USERDATA root before returning it, raising an error otherwise.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

high

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.