Back to Blog
critical SEVERITY6 min read

How path traversal happens in Python os.path and how to fix it

A critical path traversal vulnerability in the TRL backend allowed attackers to read arbitrary system files like `/etc/passwd` and `/proc/self/environ` through the gRPC fine-tuning API. The `_do_training` method passed user-controlled `dataset_source` directly to `os.path.exists()` and `load_dataset()` without validation. The fix implements strict directory containment checks using `os.path.realpath()` to ensure all file operations stay within allowed directories.

O
By Orbis AppSec
Published June 22, 2026Reviewed June 22, 2026

Answer Summary

Path traversal (CWE-22) in Python occurs when user input is passed directly to filesystem functions like `os.path.exists()` without validation. In this TRL backend vulnerability, the `_do_training` method accepted arbitrary paths via gRPC requests, enabling attackers to read sensitive files. The fix validates paths using `os.path.realpath()` and `os.path.abspath()` to ensure they remain within a configurable allowed directory (`LOCALAI_DATASET_DIR`), rejecting any path that escapes the boundary.

Vulnerability at a Glance

cweCWE-22
fixImplement directory containment check using `os.path.realpath()` against allowed directory boundary
riskAttackers can read sensitive system files (/etc/passwd, /proc/self/environ) via gRPC requests
languagePython
root causeUser-controlled `dataset_source` passed directly to `os.path.exists()` without path validation
vulnerabilityPath Traversal / Arbitrary File Read

Introduction

The TRL (Transformer Reinforcement Learning) backend in LocalAI handles fine-tuning requests via gRPC, but a critical flaw in backend/python/trl/backend.py at line 310 created a dangerous attack vector. The _do_training method accepted a dataset_source parameter from incoming StartFineTune gRPC requests and passed it directly to os.path.exists() and load_dataset() without any validation.

This meant an attacker with network access to the gRPC backend on port 50051 could specify paths like /etc/passwd or /proc/self/environ as their "dataset source" and trick the server into reading—and potentially leaking—sensitive system files. For a machine learning backend that's designed to load training data, this represents a complete breakdown of the security boundary between user-supplied input and the underlying filesystem.

The Vulnerability Explained

The Dangerous Code Pattern

The vulnerable code in _do_training looked like this:

dataset_split = request.dataset_split or "train"
if os.path.exists(request.dataset_source):
    if request.dataset_source.endswith('.json') or request.dataset_source.endswith('.jsonl'):
        dataset = load_dataset("json", data_files=request.dataset_source, split=dataset_split)
    elif request.dataset_source.endswith('.csv'):
        # ... load CSV dataset

The problem is immediately apparent: request.dataset_source comes directly from the gRPC request with zero validation. The code trusts that the client will only send legitimate dataset paths, but a malicious actor can send anything.

Attack Scenario

Here's how an attacker could exploit this:

  1. Reconnaissance: The attacker discovers the TRL gRPC backend is exposed on port 50051 (either directly or through a misconfigured network)

  2. Crafting the Request: They send a StartFineTune gRPC request with:
    dataset_source: "/proc/self/environ" dataset_split: "train" model_name: "sshleifer/tiny-gpt2" output_dir: "/tmp/output"

  3. File Access: The backend calls os.path.exists("/proc/self/environ"), which returns True. The code then attempts to load this "dataset," potentially exposing environment variables containing API keys, database credentials, or other secrets.

  4. Escalation: With access to /etc/passwd, the attacker learns system usernames. With /proc/self/environ, they might find AWS_SECRET_ACCESS_KEY, DATABASE_URL, or other sensitive environment variables that the ML backend uses.

Why This Is Critical

This isn't just a theoretical risk. The TRL backend is a Python service that likely runs with access to:
- Model weights and training data (potentially proprietary)
- API tokens for HuggingFace and other services
- Cloud credentials for distributed training
- Database connections for experiment tracking

A path traversal here gives attackers a window into all of these.

The Fix

The fix implements a directory containment check—a security pattern that ensures all file operations stay within a designated safe directory.

Before (Vulnerable)

dataset_split = request.dataset_split or "train"
if os.path.exists(request.dataset_source):
    if request.dataset_source.endswith('.json') or request.dataset_source.endswith('.jsonl'):
        dataset = load_dataset("json", data_files=request.dataset_source, split=dataset_split)

After (Fixed)

dataset_split = request.dataset_split or "train"
if os.path.exists(request.dataset_source):
    _allowed_dir = os.path.realpath(os.path.abspath(os.environ.get("LOCALAI_DATASET_DIR", os.getcwd())))
    _real_path = os.path.realpath(os.path.abspath(request.dataset_source))
    if not (_real_path == _allowed_dir or _real_path.startswith(_allowed_dir + os.sep)):
        raise ValueError("Dataset source path is outside the allowed directory")
    if request.dataset_source.endswith('.json') or request.dataset_source.endswith('.jsonl'):
        dataset = load_dataset("json", data_files=request.dataset_source, split=dataset_split)

How the Fix Works

  1. os.path.abspath(): Converts any relative path to an absolute path based on the current working directory

  2. os.path.realpath(): Resolves all symbolic links to get the true canonical path—this prevents symlink-based bypasses

  3. Boundary Check: The code verifies that the resolved path either equals the allowed directory or starts with the allowed directory followed by a path separator (os.sep). The separator check is crucial—without it, /allowed/dir_malicious would pass a check for /allowed/dir

  4. Configurable Allowed Directory: The LOCALAI_DATASET_DIR environment variable lets operators define where datasets should live, defaulting to the current working directory

The Output Path Fix

The same vulnerability existed in the ExportModel method for output paths. The fix applies identical logic:

_allowed_output_dir = os.path.realpath(os.path.abspath(os.environ.get("LOCALAI_OUTPUT_DIR", os.getcwd())))
_real_output_path = os.path.realpath(os.path.abspath(output_path))
if not (_real_output_path == _allowed_output_dir or _real_output_path.startswith(_allowed_output_dir + os.sep)):
    raise ValueError("Output path is outside the allowed directory")
output_path = _real_output_path

This prevents attackers from writing model exports to arbitrary locations like /etc/cron.d/ or overwriting system files.

Key Takeaways

  • The _do_training method's direct use of request.dataset_source in os.path.exists() created a critical file read vulnerability—always validate paths before filesystem operations

  • gRPC services are network-exposed attack surfaces—treat all request parameters as potentially malicious, even in internal ML pipelines

  • os.path.realpath() + boundary checking is the correct fix pattern—simple string filtering for ../ is insufficient and can be bypassed

  • Both input paths (dataset_source) and output paths (output_path) need validation—the fix addressed both vectors in _do_training and ExportModel

  • Environment-configurable allowed directories (LOCALAI_DATASET_DIR, LOCALAI_OUTPUT_DIR) enable secure deployment flexibility without hardcoding paths

How Orbis AppSec Detected This

  • Source: The dataset_source field from incoming StartFineTune gRPC requests in backend/python/trl/backend.py
  • Sink: os.path.exists(request.dataset_source) at line 310 and subsequent load_dataset() calls
  • Missing control: No path validation or directory containment check before filesystem access
  • CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
  • Fix: Added os.path.realpath() resolution and boundary validation against LOCALAI_DATASET_DIR before any filesystem operations

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 remain one of the most common and dangerous security issues in web services and APIs. This TRL backend vulnerability demonstrates how easily they can slip into ML infrastructure code, where the focus is often on model performance rather than security hardening.

The fix is straightforward but must be applied correctly: resolve paths to their canonical form, check against an allowed directory boundary, and include the path separator in your prefix check. These three steps—implemented consistently across all file operations—form a robust defense against path traversal attacks.

For teams building ML pipelines and fine-tuning services, remember that your training infrastructure is a high-value target. Attackers who compromise these systems gain access to models, training data, and the credentials that connect to your broader infrastructure.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #10422

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

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.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.