Back to Blog
high SEVERITY6 min read

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.

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

This is a **path traversal vulnerability (CWE-22)** in Python's `posttrain_runner.py` where user-controlled input (`base_ckpt`) was concatenated directly into a file path for `open()` without sanitization. The vulnerability allowed attackers to read arbitrary files using `../` sequences. The fix uses `os.path.realpath()` to resolve and validate that resolved paths start within `os.getcwd()`, rejecting any traversal attempts with `sys.exit(2)`.

Vulnerability at a Glance

cweCWE-22
fixAdded `os.path.realpath()` resolution and prefix check against `safe_root` before file operations
riskArbitrary file read via directory traversal sequences in ML pipeline checkpoint loading
languagePython
root cause`base_ckpt` parameter concatenated directly into file path without path validation
vulnerabilityPath Traversal

Title: How Path Traversal Happens in Python and How to Fix It

SEO Title: posttrain_runner.py Path Traversal: Safe Fix

SEO Description: Learn how unvalidated input in check_lineage() created a path traversal vulnerability. See the os.path.realpath() fix that prevents arbitrary file reads in ML pipeline scripts.


Summary

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.


Answer Summary

This is a path traversal vulnerability (CWE-22) in Python's posttrain_runner.py where user-controlled input (base_ckpt) was concatenated directly into a file path for open() without sanitization. The vulnerability allowed attackers to read arbitrary files using ../ sequences. The fix uses os.path.realpath() to resolve and validate that resolved paths start within os.getcwd(), rejecting any traversal attempts with sys.exit(2).


Introduction

In a machine learning pipeline repository, we discovered a HIGH severity path traversal vulnerability in skills/packs/pipeline-phase-8-post-train-runs/scripts/posttrain_runner.py at line 23. The check_lineage() function, responsible for validating model checkpoint lineage before training, accepted a base_ckpt parameter and directly concatenated it into a file path without any validation.

The vulnerable code looked innocent enough:

meta = base_ckpt + ".manifest.json"
if os.path.exists(meta):
    d = json.load(open(meta, encoding="utf-8"))

This pattern—concatenating user input into a filesystem path—is one of the most common sources of path traversal vulnerabilities. In ML pipelines where checkpoints may be specified via configuration files, CI/CD parameters, or even derived from external data sources, this creates a significant attack surface. An attacker who controls the base_ckpt value could traverse outside the intended directory and read arbitrary files on the system.


The Vulnerability Explained

The Problematic Code Pattern

The original check_lineage() function contained this vulnerable pattern:

def check_lineage(base_ckpt):
    meta = base_ckpt + ".manifest.json"  # Line 23: DANGEROUS
    if os.path.exists(meta):
        d = json.load(open(meta, encoding="utf-8"))  # Line 26: EXPLOITABLE

What's wrong here?

  1. Direct concatenation: The base_ckpt parameter is concatenated directly with ".manifest.json" using string concatenation (+)
  2. No path normalization: The path is not resolved to its canonical form before use
  3. Unrestricted file access: Any path string is accepted, including those containing directory traversal sequences like ../../../etc/passwd

How Could This Be Exploited?

Consider an ML pipeline that loads checkpoint configurations from external sources. An attacker could provide a malicious base_ckpt value:

# Malicious input
base_ckpt = "../../../etc/passwd"

# Resulting path
meta = "../../../etc/passwd.manifest.json"

Wait—that doesn't directly hit /etc/passwd. But with more creative input:

# More dangerous input
base_ckpt = "../../../etc/passwd%00"  # Null byte injection on some systems
# Or simply:
base_ckpt = "/etc/passwd"  # Absolute path bypass

The real danger emerges when base_ckpt is derived from user-controlled configuration or when the pipeline processes checkpoints from untrusted sources. An attacker could:

  1. Read sensitive configuration files: Access cloud credentials, API keys, or database connection strings stored in configuration files
  2. Exfiltrate model artifacts: Read proprietary model weights or training data from other directories
  3. Bypass lineage checks: By controlling which manifest file is read, an attacker could falsify lineage verification

Real-World Impact for This Application

This vulnerability exists in a post-training pipeline script that validates model checkpoint lineage. The check_lineage() function ensures that parent models passed quality gates before being used for fine-tuning. If compromised:

  • Training integrity: Attackers could bypass lineage validation by providing a crafted manifest path
  • Data exfiltration: Sensitive training data or model checkpoints from other pipeline stages could be accessed
  • Supply chain attacks: Malicious checkpoints could be introduced into the training pipeline

The Fix

The remediation implements defense in depth with multiple security controls:

Before (Vulnerable)

def check_lineage(base_ckpt):
    meta = base_ckpt + ".manifest.json"
    if os.path.exists(meta):
        d = json.load(open(meta, encoding="utf-8"))

After (Fixed)

def check_lineage(base_ckpt):
    safe_root = os.path.realpath(os.getcwd())
    resolved = os.path.realpath(base_ckpt)
    if not resolved.startswith(safe_root + os.sep) and resolved != safe_root:
        print("REJECT: path traversal — base path must be within working directory")
        sys.exit(2)
    meta = resolved + ".manifest.json"
    if os.path.exists(meta):
        with open(meta, encoding="utf-8") as f:
            d = json.load(f)

Security Improvements Explained

Change Security Benefit
os.path.realpath(os.getcwd()) Establishes a trusted root directory as the security boundary
os.path.realpath(base_ckpt) Resolves all symlinks and ../ sequences to canonical absolute paths
startswith(safe_root + os.sep) check Enforces that resolved paths must be within the working directory
sys.exit(2) on failure Fails securely—rejects suspicious input rather than attempting to sanitize
with open(...) context manager Proper resource handling (secondary hardening)

The fix uses os.sep (the platform-specific path separator) rather than hardcoded / to ensure correct behavior across Windows and Unix systems. The check resolved != safe_root handles the edge case where base_ckpt points exactly to the working directory.


Prevention & Best Practices

For Python File Operations

  1. Always validate paths before use
    ```python
    import os

def safe_open(user_path, safe_root):
resolved = os.path.realpath(user_path)
if not resolved.startswith(os.path.realpath(safe_root) + os.sep):
raise ValueError("Path traversal detected")
return open(resolved, 'r')
```

  1. Use pathlib for modern path handling
    ```python
    from pathlib import Path

safe_root = Path.cwd().resolve()
target = (safe_root / user_input).resolve()
if not target.is_relative_to(safe_root):
raise ValueError("Traversal attempt")
```

  1. Avoid string concatenation for paths
    - Use os.path.join() or pathlib.Path operators
    - Still validate after joining—os.path.join() doesn't prevent traversal!

Detection Tools

Tool Rule/Feature URL
Semgrep utils.custom.path-traversal-open https://semgrep.dev/r?q=path-traversal
Bandit B605: start_process_with_a_shell (related) https://bandit.readthedocs.io/
CodeQL py/path-injection https://codeql.github.com/

Security Standards

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • OWASP Top 10 2021: A01:2021 – Broken Access Control (includes path traversal)
  • CWE-73: External Control of File Name or Path

Key Takeaways

  • Never concatenate user-controlled base_ckpt directly into file paths in ML pipeline scripts — always resolve and validate against a safe root directory
  • os.path.realpath() is essential for path validation — it eliminates symlinks and normalizes ../ sequences that could bypass simple string checks
  • The check_lineage() function now enforces a workspace boundary — all checkpoint paths must resolve within os.getcwd() or the pipeline exits with code 2
  • Use explicit separator checking with os.sep — platform-aware validation prevents bypasses on Windows (\) vs. Unix (/) systems
  • Fail securely with sys.exit(2) — reject traversal attempts immediately rather than attempting dangerous sanitization

How Orbis AppSec Detected This

Aspect Details
Source The base_ckpt function parameter in check_lineage(base_ckpt)
Sink open(meta, encoding="utf-8") at line 26, where meta = base_ckpt + ".manifest.json"
Missing control No path resolution, normalization, or directory boundary validation before file open
CWE CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Fix Added os.path.realpath() resolution with safe_root prefix validation and secure exit on violation

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 prevalent in Python applications, particularly in data processing and ML pipeline code where file paths are frequently constructed from external input. The fix in posttrain_runner.py demonstrates that robust protection requires more than simple string checks—it demands canonical path resolution and explicit boundary validation.

By adopting os.path.realpath()-based validation as a standard pattern for all file operations, development teams can eliminate an entire class of vulnerabilities from their codebase. The key is treating all external path input as untrusted and enforcing strict workspace boundaries before any filesystem access.


References

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'): https://cwe.mitre.org/data/definitions/22.html
  • OWASP Path Traversal Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Path_Traversal_Prevention_Cheat_Sheet.html
  • Python os.path.realpath() documentation: https://docs.python.org/3/library/os.path.html#os.path.realpath
  • Semgrep path traversal rules: https://semgrep.dev/r?q=path-traversal
  • GitHub PR: harden: add path validation in posttrain_runner.py...

Frequently Asked Questions

What is path traversal?

Path traversal (or directory traversal) is a vulnerability where attackers supply input containing `../` or similar sequences to access files outside the intended directory, potentially reading sensitive system files.

How do you prevent path traversal in Python?

Use `os.path.realpath()` to resolve canonical paths, then validate with `startswith()` against a safe root directory. Never concatenate user input directly into file paths.

What CWE is path traversal?

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

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

No. `os.path.join()` does not prevent traversal — it simply concatenates paths. Attackers can still inject `../` sequences that `os.path.join()` will honor.

Can static analysis detect path traversal?

Yes. Tools like Semgrep can detect when user-controlled input flows into `open()` calls without path validation, as seen with the `utils.custom.path-traversal-open` rule used here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

high

How unrestricted file upload via extension-only validation happens in Deno/JavaScript and how to fix it

The review image upload handler in this Deno-based app trusted the client-supplied filename extension to decide whether a file was a "safe" image, without ever inspecting the actual file bytes. The fix adds magic-byte signature verification for PNG, JPEG, GIF, and WEBP formats before the file is written to disk, closing the door on disguised executables and malicious payloads.

critical

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

A critical zip-slip vulnerability (CVE-2026-53486) in the `@xhmikosr/decompress` package allowed crafted archives to write files outside the intended extraction directory, enabling arbitrary file read/write on the host. The fix upgrades `@xhmikosr/decompress` from 5.0.0 to 10.2.1/11.1.3 and its dependency `@xhmikosr/bin-wrapper` from ^5.0.0 to ^13.2.0, closing the path-sanitization gap in the underlying extractors.

high

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.

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.

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 Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.