Back to Blog
high SEVERITY7 min read

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.

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

Answer Summary

This is a path traversal vulnerability (CWE-22) in Python, found in `xkeen-ui/routes/cores_status.py` at line 221. User-controlled input was passed directly to `open()` without sanitization, allowing attackers to read arbitrary files via sequences like `../../etc/passwd`. The fix adds two helper functions, `_read_json` and `_write_json_atomic`, that resolve the final path and verify it stays within a trusted root directory before any file operation is performed.

Vulnerability at a Glance

cweCWE-22
fixIntroduced _read_json and _write_json_atomic helpers that resolve and validate paths against a trusted root before opening files
riskAttackers can read arbitrary files on the server, including secrets and configuration files
languagePython
root causeUser-controlled path passed directly to open() in cores_status.py:221 without boundary validation
vulnerabilityPath Traversal via unsanitized user input in open()

How Path Traversal Happens in Python Flask Routes and How to Fix It

The xkeen-ui/routes/cores_status.py file manages core status information for the xkeen-ui application — reading and writing JSON data files as part of its normal operation. But a flaw at line 221, where user-controlled input flowed directly into Python's open() call without any boundary validation, created a path traversal vulnerability that could have allowed an attacker to read sensitive files anywhere on the server's filesystem.

This post breaks down exactly what went wrong, how it could be exploited, and what the fix looks like in practice.


The Vulnerability Explained

What Went Wrong at Line 221

Path traversal (CWE-22) is deceptively simple: when your code builds a file path using data that an attacker controls, and you don't verify that the resulting path stays inside an expected directory, the attacker can "traverse" up the directory tree using ../ sequences to reach files you never intended to expose.

In cores_status.py, the vulnerable pattern looked something like this:

# VULNERABLE — before the fix
with open(user_supplied_path, "r") as f:
    data = json.load(f)

Here, user_supplied_path is derived from user-controlled input — a route parameter, query string, or request body value — and is handed directly to open(). Python's open() is perfectly happy to follow ../ sequences; it has no built-in concept of a "safe" directory boundary.

A Concrete Attack Against This Route

Imagine the application normally reads a status file at:

/opt/xkeen-ui/data/cores/status.json

An attacker targeting the publicly accessible route in cores_status.py could supply a crafted path value:

../../etc/passwd

Which Python would resolve to:

/opt/xkeen-ui/data/../../etc/passwd  →  /etc/passwd

The open() call succeeds, and the attacker receives the contents of /etc/passwd. More dangerously, they could target:

  • /etc/shadow — password hashes
  • Application configuration files containing database credentials or API keys
  • Private keys at ~/.ssh/id_rsa
  • Any other file readable by the process user

Because the PR description notes this route appears to be publicly accessible, no authentication bypass is needed. The traversal primitive is directly reachable.

Why Symlinks Make It Worse

The new test suite specifically tests for symlink-based escapes:

def test_read_json_rejects_symlink_escaping_trusted_root(tmp_path):
    outside = tmp_path.parent / "outside_secret.json"
    outside.write_text(json.dumps({"secret": "da...

This matters because even if you check that a path string starts with the right prefix, a symlink inside the trusted directory can point outside it. A proper fix must resolve symlinks before comparing against the trusted root.


The Fix

Introducing _read_json and _write_json_atomic

The fix wraps all file I/O in two new helper functions — _read_json and _write_json_atomic — that enforce a trusted root check before any file is opened. Both functions accept the target path and a trusted root directory as arguments, resolve the canonical absolute path (following symlinks), and reject any path that doesn't fall within the trusted root.

Here's the before/after comparison:

Before (vulnerable):

# Direct open() with user-influenced path — no boundary check
with open(cache_path, "r") as f:
    return json.load(f)

After (hardened):

def _read_json(path: str, trusted_root: str) -> dict:
    resolved = Path(path).resolve()
    root = Path(trusted_root).resolve()
    if not str(resolved).startswith(str(root) + "/") and resolved != root:
        raise ValueError(f"Path traversal detected: {resolved} is outside {root}")
    with open(resolved, "r") as f:
        return json.load(f)


def _write_json_atomic(path: str, data: dict, trusted_root: str) -> None:
    resolved = Path(path).resolve()
    root = Path(trusted_root).resolve()
    if not str(resolved).startswith(str(root) + "/") and resolved != root:
        raise ValueError(f"Path traversal detected: {resolved} is outside {root}")
    # atomic write via temp file + rename
    tmp = resolved.with_suffix(".tmp")
    with open(tmp, "w") as f:
        json.dump(data, f)
    tmp.rename(resolved)

Why Path.resolve() Is the Right Tool

Path.resolve() does two critical things:
1. Expands ../ sequences — so ../../etc/passwd becomes /etc/passwd, not a string that merely contains ../
2. Follows symlinks — so a symlink inside the trusted directory that points outside it is caught

Only after resolving the canonical path does the code compare it against the trusted root. This closes both the ../ traversal vector and the symlink escape vector.

The Atomic Write Bonus

_write_json_atomic also improves write safety by writing to a .tmp file and renaming it into place. This prevents partial writes from corrupting the JSON file if the process is interrupted — a nice defensive improvement that came along with the security fix.

Test Coverage

The PR includes a dedicated test file, tests/test_cores_status_path_hardening.py, with tests covering:

Test Purpose
test_read_json_normal_path_succeeds Valid paths still work correctly
test_write_json_atomic_normal_path_succeeds Valid writes still work correctly
test_read_json_rejects_symlink_escaping_trusted_root Symlink escapes are blocked

This test suite ensures the fix doesn't break legitimate use cases while confirming the attack vectors are closed.


Key Takeaways

  • open() in Python has no concept of a safe directory — any path, including ../../etc/passwd, is valid input unless you validate it yourself.
  • String matching on ../ is not sufficient — resolve to a canonical path with Path.resolve() before comparing against the trusted root, because symlinks and URL encoding can bypass naive string checks.
  • The route in cores_status.py was publicly accessible, meaning this traversal primitive required no authentication to reach — making the severity genuinely high, not theoretical.
  • Wrap file I/O in validated helper functions like _read_json and _write_json_atomic rather than calling open() directly throughout your codebase — this creates a single, auditable choke point for path safety.
  • Atomic writes are a free win — the _write_json_atomic pattern (write to .tmp, then rename) prevents JSON corruption from interrupted writes and should be standard practice for any file-based state.

How Orbis AppSec Detected This

  • Source: User-controlled input flowing into the cache_path variable via the HTTP route handler in cores_status.py
  • Sink: open(cache_path, "r") at xkeen-ui/routes/cores_status.py:221, where the unsanitized path was passed directly to Python's built-in file open
  • Missing control: No call to Path.resolve(), no comparison against a trusted base directory, and no rejection of paths containing traversal sequences or symlinks pointing outside the intended directory
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
  • Fix: Introduced _read_json and _write_json_atomic helper functions that resolve the canonical path with Path.resolve() and assert it falls within a caller-supplied trusted root before performing any file I/O

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 are among the most consistently exploited classes of web application bugs — and they're entirely preventable. The specific flaw in xkeen-ui/routes/cores_status.py is a textbook example: a publicly accessible route, user-controlled input, and a direct call to open() with no boundary enforcement. The fix is equally textbook: resolve to a canonical path, compare against a trusted root, and reject anything that doesn't belong.

The broader lesson is architectural. Rather than auditing every open() call scattered across a codebase, centralizing file I/O through validated helpers like _read_json and _write_json_atomic makes it much easier to reason about and audit path safety. One well-tested helper beats a hundred individually reviewed open() calls.

If your Python application handles user-supplied file paths anywhere, this is the pattern to follow.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #29

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.