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.


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...

Frequently Asked Questions

What is path traversal?

Path traversal is an attack where an attacker manipulates file path input (using sequences like `../`) to access files outside the intended directory, potentially reading sensitive data or configuration files.

How do you prevent path traversal in Python?

Validate and normalize all user-controlled file paths using `os.path.abspath()`, `os.path.realpath()`, or `pathlib.Path`, and verify the resolved path is within an allowed directory using `os.path.commonpath()` before opening files.

What CWE is path traversal?

Path traversal is primarily CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and sometimes overlaps with CWE-23 (Relative Path Traversal).

Is using `os.path.join()` enough to prevent path traversal?

No. `os.path.join()` alone is insufficient because `os.path.join('/safe/dir', '../../etc/passwd')` will still produce a path outside the intended directory. You must normalize and validate the final path.

Can static analysis detect path traversal?

Yes. Modern static analysis tools like Semgrep, Bandit, and commercial SAST platforms can detect path traversal patterns by flagging user input flowing directly into file operations without validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #36

Related Articles

high

How command injection happens in Node.js child_process spawn calls and how to fix it

A benchmarking helper in `bench/lib/actor.js` passed an unvalidated executable path from upstream pipeline results directly into `child_process.spawn()`. The fix resolves the path and enforces that it lives inside the sandboxed stage directory before execution, closing off a path-traversal-driven command injection primitive.

high

How path traversal happens in Python and how to fix it

A high-severity path traversal vulnerability in `posttrain_runner.py` allowed arbitrary file reads through the `base_ckpt` parameter. The fix implements `os.path.realpath()` validation to ensure all file paths remain within the working directory, preventing attackers from accessing sensitive system files.

critical

How Path Traversal and Resource Exhaustion happen in Node.js HTTP servers and how to fix them

A critical security vulnerability in `wasm-build/server.js` allowed attackers to read arbitrary files outside the web root via path traversal, while simultaneously leaving the server open to resource exhaustion through unbounded concurrent connections. The fix sanitizes URL paths before joining them to the filesystem and enforces strict connection and timeout limits to prevent denial-of-service attacks.

high

How Path Traversal happens in Node.js tmp package and how to fix it

The tmp package version 0.0.33 contained a high-severity path traversal vulnerability (CVE-2026-44705) that allowed attackers to escape temporary directories through unsanitized prefix and postfix parameters. This reddit-app project was upgraded from tmp 0.0.33 to 0.2.7, which implements proper input sanitization to prevent directory traversal attacks and removes the deprecated os-tmpdir dependency.

medium

How Path Traversal and Filename Injection Happens in Python File Handling and How to Fix It

A medium-severity path traversal vulnerability in `PainterNode/painter_node.py` allowed attackers to reference files outside the intended directory by exploiting a broken `isFileName()` validation function. The original logic used incorrect boolean operators, meaning the filename guard never actually blocked malicious inputs like `../../../etc/passwd` or paths containing backslashes. The fix rewrites the condition with proper logic and adds explicit checks for path separator characters and direc

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr