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
Published August 31, 2026Reviewed 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

critical

How path traversal happens in PHP virtual filesystem adapters and how to fix it

A critical path traversal flaw in `VirtualAdapter.php`'s `resolveMount()` method allowed attackers to escape mounted directory boundaries using sequences like `../../../etc/passwd`. The fix introduces `PathPolicy::normalizeRelative()` to sanitize the remaining path segment before it ever reaches the underlying storage adapter.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

high

How Trust-Prefix Bypass via Path Traversal Happens in Python Copier and How to Fix It

CVE-2026-53951 is a high-severity path traversal vulnerability in Copier 9.15.0 that allowed attackers to bypass trust-prefix checks and execute tasks without user confirmation. Upgrading to Copier 9.15.2 eliminates this attack vector by properly validating file paths before task execution.

critical

How Path Traversal in basic-ftp Leads to File Overwrite Attacks and How to Fix It

CVE-2026-27699 is a critical path traversal vulnerability in basic-ftp versions before 5.3.1 that allows attackers to overwrite arbitrary files on the system by crafting malicious file paths. This vulnerability was fixed by upgrading basic-ftp and enforcing strict version constraints across dependent packages. Understanding this attack and its mitigation is essential for developers using FTP libraries in production environments.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.

critical

How Path Traversal happens in Node.js Express servers and how to fix it

A path traversal vulnerability in `src/server.js` allowed attackers to escape the intended wiki directory by sending encoded traversal sequences through the `/api/pages/:slug(*)` wildcard endpoint. The flawed `startsWith` boundary check could be bypassed after `decodeURIComponent` processing, potentially exposing arbitrary files on the server. The fix replaces the inline filesystem logic with a dedicated `readWikiPage()` function that enforces proper path validation.