Path Traversal in Flask Applications
Flask applications can be vulnerable to path traversal attacks, which may allow attackers to read or even overwrite unauthorized files depending on how the application handles file paths and user input.
Many web applications, including Flask apps, often read files from local directories and serve their contents as part of normal application functionality.
However, if the filename being read is partially or fully controlled by the user, and if the application does not properly sanitize or validate this input, an attacker may be able to manipulate the file path using traversal sequences (such as ../) to escape the intended directory and access sensitive files elsewhere on the system.
We have a dedicated page that covers Path Traversal vulnerabilities in Python in more detail, which can be found Insecure File Access and Path Traversal in Python.
Here’s an example of a Flask application that is vulnerable to path traversal:
from flask import Flask, request
import os
app = Flask(__name__)
def read_file(file_name):
full_file = "files/%s" % file_name
# Check if the path exists
if os.path.exists(full_file):
# Read the file
f = open(full_file)
data = f.read()
return data
else:
return "No Content!"
@app.route('/read')
def readf():
user_file = request.args.get("file_name")
file_to_open = read_file(user_file)
return file_to_open
if __name__ == '__main__':
app.run(debug=True)
In this example:
-
The application expects a query parameter called
file_name, which specifies the file to read. -
The file path is constructed by directly concatenating the user input into the file path.
-
There’s no input validation or sanitization to restrict or normalize the provided file name
If an attacker provides input like:
/read?file_name=../../../../../etc/passwd
The constructed path becomes:
files/../../../../../etc/passwd
In this case, the payload will resolve to /etc/passwd, and this absolute path will be passed directly to the open() function.
It’s also important to highlight that while the application performs a basic check using os.path.exists() to verify that the file exists before reading it, this function does not prevent path traversal.
os.path.exists() simply checks whether the resolved path exists on the file system, regardless of how it was constructed, and will still accept paths that include traversal sequences like ../.
This means the existence check offers no protection against directory traversal attacks in this context.
Why path traversal matters from an offensive security perspective
I go after path traversal because in a Flask app it usually yields an arbitrary file read with no authentication wrapped around it, and on a Python service the most valuable files are predictable. app.py, config.py, .env, settings.py, and instance/ give me the SECRET_KEY, database URIs, and API tokens, and a leaked SECRET_KEY lets me forge session cookies and walk straight into authenticated functionality. /proc/self/environ and /proc/self/cmdline add runtime secrets and the exact launch path. When the same handler can write rather than read, traversal escalates from disclosure to overwriting code or config, which is a path to execution.
The reason it persists is that existence and access checks get mistaken for containment checks. The tells I audit for:
- User input concatenated into a path with
%,+,os.path.join, or an f-string.os.path.join(base, user)is not safe; an absolute or..-laden segment escapes the base entirely. os.path.exists,os.path.isfile, or a try/except aroundopen()used as the only guard. These confirm the file exists, not that it lives under the intended directory, so traversal sails through.open(),send_file, andsend_from_directoryfed raw request values. Download, export, template-by-name, and avatar handlers are the usual carriers.- Validation that checks the raw string instead of the resolved path. Blocking the literal
../misses encoded variants, absolute paths, and symlinks; onlyrealpathplus a base-directory check actually contains it.
The defender takeaway: resolve the final path and prove it stays inside an allowlisted base before opening it. The language-level mechanics are covered in Insecure File Access and Path Traversal in Python.
Proof of exploitation
Run the lab app (PyFuLabs/flask-fu/flask-path-traversal) and walk out of the intended directory with ../ sequences:
curl -sG "http://pyfu.local/flask-fu/flask-path-traversal/read" \
--data-urlencode "file_name=../../../../etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
The os.path.exists check passed because the traversed path exists, and the file was read and returned.
Mitigation
The fix is to stop trusting the filename. Resolve the requested path with os.path.realpath and confirm it remains within an allowlisted base directory using os.path.commonpath before opening it, rejecting anything that escapes, since os.path.exists checks existence and not containment. Where possible, map an opaque identifier supplied by the user onto a known-safe path on the server so the raw ../ sequence never reaches open() at all.