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.

Prevention & Best Practices

1. Always Validate File Paths from User Input

Never trust paths from external sources. Always:
- Resolve to canonical paths with os.path.realpath()
- Check against an allowed directory boundary
- Use the path separator in your prefix check

def is_safe_path(base_dir: str, user_path: str) -> bool:
    """Check if user_path is safely within base_dir."""
    base = os.path.realpath(os.path.abspath(base_dir))
    target = os.path.realpath(os.path.abspath(user_path))
    return target == base or target.startswith(base + os.sep)

2. Use Allowlists for Dataset Sources

For ML backends, consider maintaining an allowlist of valid dataset identifiers:

ALLOWED_DATASETS = {"imdb", "squad", "glue", "custom_dataset_v1"}

if request.dataset_source in ALLOWED_DATASETS:
    dataset = load_dataset(request.dataset_source, split=dataset_split)
elif is_safe_path(DATASET_DIR, request.dataset_source):
    # Load from local file

3. Principle of Least Privilege

Run ML backends with minimal filesystem permissions. Use containerization or chroot to limit what the process can access even if path validation fails.

4. Input Validation at the API Boundary

Validate inputs as early as possible—ideally in the gRPC service definition or a middleware layer:

def validate_dataset_source(source: str) -> bool:
    # Reject absolute paths
    if os.path.isabs(source):
        return False
    # Reject path traversal sequences
    if '..' in source:
        return False
    return True

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.

References

Frequently Asked Questions

What is path traversal?

Path traversal is a vulnerability where attackers manipulate file paths to access files outside intended directories, often using sequences like `../` or absolute paths like `/etc/passwd`.

How do you prevent path traversal in Python?

Use `os.path.realpath()` and `os.path.abspath()` to resolve the canonical path, then verify it starts with your allowed base directory using string prefix checking with the path separator.

What CWE is path traversal?

Path traversal is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Is checking for `../` enough to prevent path traversal?

No. Attackers can bypass simple string checks using absolute paths (`/etc/passwd`), URL encoding, or symlinks. Always resolve to the real path and validate against an allowed directory.

Can static analysis detect path traversal?

Yes. Static analysis tools can trace data flow from user inputs to filesystem functions and flag cases where validation is missing before calls like `os.path.exists()` or `open()`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #10422

Related Articles

critical

How Insecure API Key Transmission Happens in JavaScript Browser Extensions and How to Fix It

A critical vulnerability in `utils/common.js` allowed API keys to be transmitted over unencrypted HTTP connections to remote servers, exposing them to network interception. The `buildModelApiRequest` function at line 490 constructed API requests without validating the transport protocol, enabling man-in-the-middle attacks. The fix enforces HTTPS for all remote API endpoints while preserving HTTP access for local development servers on loopback addresses.

critical

How URL Injection via Unvalidated User Input happens in Node.js and how to fix it

A critical URL injection vulnerability in the QQ info lookup feature allowed attackers to manipulate API request parameters by sending specially crafted messages. Without proper input validation, user-controlled data was directly embedded into external API URLs, potentially exposing sensitive authentication credentials (skey and pskey) to attacker-controlled servers.

high

How Denial of Service via Exponential-Time Complexity Happens in Node.js Dependencies and How to Fix It

A high-severity denial of service vulnerability (CVE-2026-14257) was discovered in the brace-expansion package within the zeroshot-oecp Docker container's dependency tree. The vulnerability allows attackers to craft malicious input patterns that trigger exponential-time processing, potentially freezing or crashing Node.js applications. This fix upgrades the nested brace-expansion dependency to version 5.0.9 using a targeted Dockerfile modification.

high

How Cache-Control Header Mishandling Happens in Node.js HTTP Clients and How to Fix It

CVE-2026-13697 is a high-severity vulnerability in undici, the popular Node.js HTTP client, where the cache interceptor fails to properly validate malformed `Cache-Control: private` directives. This could allow sensitive cached responses to be served to unauthorized users. The fix upgrades undici from 7.28.0 to 7.29.0 (and 6.27.0 to 6.28.0) across the dependency tree, including using npm overrides to patch transitive dependencies.

medium

How XML Entity Expansion Denial of Service happens in Node.js and how to fix it

A critical denial of service vulnerability (CVE-2026-33036) was discovered in fast-xml-parser versions prior to 5.5.6 and 4.5.5, allowing attackers to bypass entity expansion limits and crash Node.js applications through malicious XML payloads. This fix upgrades the dependency in the scripts directory to patched versions, protecting build pipelines and any runtime XML processing from resource exhaustion attacks.

critical

How Arbitrary Code Execution via Command Injection Happens in Node.js shell-quote and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the popular Node.js `shell-quote` package (versions prior to 1.8.4) where unescaped line terminators allowed attackers to inject and execute arbitrary shell commands. The fix upgrades `shell-quote` from version 1.8.1 to 1.8.4, which properly escapes line terminator characters (such as `\n`, `\r`, `\u2028`, and `\u2029`) before passing strings to the shell. This dependency was present in the project's `package-lock.json`