# PYTHON-FLASK-SEC-017: Flask Insecure Static File Serve

> **Severity:** MEDIUM | **CWE:** CWE-73 | **OWASP:** A01:2021

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-017
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-017 --project .`

## Description

This rule detects calls to Flask's send_file() and send_from_directory() functions, which
serve files directly from the server's file system in HTTP responses. When the filename or
path argument to these functions originates from user input (request parameters, URL path
segments, form fields), the application is vulnerable to path traversal attacks.

Path traversal (also called directory traversal) occurs when an attacker supplies sequences
like ../../../etc/passwd or URL-encoded equivalents (%2e%2e%2f) as the filename. If the
application does not sanitize the filename before passing it to send_from_directory() or
send_file(), the server will read and return files outside the intended directory -- including
application source code, configuration files, private keys, and system files.

send_from_directory() provides partial protection by joining the directory and filename
arguments, but it does not prevent traversal if the filename itself contains ../ sequences
unless werkzeug.utils.safe_join() or secure_filename() is applied first. send_file() with
an absolute path constructed from user input is even more dangerous.

The detection uses Or(calls("send_from_directory"), calls("flask.send_from_directory"),
calls("send_file"), calls("flask.send_file")) to flag any use of these functions for
manual review. Not every call is vulnerable -- serving a hardcoded filename is safe -- but
every call warrants inspection to verify the filename argument cannot be user-controlled.


## Vulnerable Code

```python
from flask import Flask, request, send_from_directory

app = Flask(__name__)

@app.route('/files')
def serve_file():
    filename = request.args.get('file')
    return send_from_directory('/uploads', filename)
```

## Secure Code

```python
import os
from flask import Flask, request, send_from_directory, abort
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_FOLDER = '/var/app/uploads'

@app.route('/download/<filename>')
def download_file(filename):
    # SAFE: Sanitize the filename to remove path traversal sequences.
    # secure_filename() strips directory components and dangerous characters.
    safe_name = secure_filename(filename)
    if not safe_name:
        abort(400)  # Reject empty or fully-stripped filenames

    # Verify the file exists in the intended directory before serving
    file_path = os.path.join(UPLOAD_FOLDER, safe_name)
    if not os.path.exists(file_path):
        abort(404)

    # send_from_directory is safer than send_file because it joins
    # the directory path server-side, but still requires a sanitized filename.
    return send_from_directory(UPLOAD_FOLDER, safe_name, as_attachment=True)

# NEVER do this:
# return send_file(request.args.get('file'))  # Direct user input to send_file

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, Or, QueryType


@python_rule(
    id="PYTHON-FLASK-SEC-017",
    name="Flask Insecure Static File Serve",
    severity="MEDIUM",
    category="flask",
    cwe="CWE-22",
    tags="python,flask,path-traversal,static-files,CWE-22",
    message="send_from_directory() with user input. Use werkzeug.utils.secure_filename().",
    owasp="A01:2021",
)
def detect_flask_insecure_static_serve():
    """Detects send_from_directory() usage (audit for user-controlled filename)."""
    return Or(
        calls("send_from_directory"),
        calls("flask.send_from_directory"),
        calls("send_file"),
        calls("flask.send_file"),
    )
```

## How to Fix

- Always sanitize filenames from user input with werkzeug.utils.secure_filename() before passing to send_file() or send_from_directory(). secure_filename() strips all directory components and dangerous characters.
- Verify that the resolved file path is inside the intended directory by checking os.path.realpath(path).startswith(os.path.realpath(base_dir)) after joining.
- Prefer serving user-uploaded files by a server-generated identifier (UUID) stored in a database rather than by the original filename. Map UUID to filename server-side.
- Set the as_attachment=True flag when serving user-uploaded files to force the browser to download rather than render them, reducing the risk of stored XSS via uploaded HTML/SVG files.
- Use a dedicated file storage service (object storage) or a web server (nginx) for static file serving instead of routing all file downloads through Flask application code.

## Security Implications

- **Arbitrary File Read via Path Traversal:** An attacker who can control the filename argument can read any file readable by the
web server process. Common targets include /etc/passwd, /etc/shadow (if the server
runs as root), application source code, .env files with credentials, SSL private keys,
and database files. In Flask applications, the config object and SECRET_KEY are
frequently stored in files adjacent to the application.

- **Source Code and Configuration Disclosure:** Flask applications commonly store configuration in app.cfg, .env, or config.py files
in the project root. A path traversal in a file-serving route can expose these files
directly, leaking database credentials, API keys, and Flask's SECRET_KEY (which enables
session forgery).

- **Private Key and Certificate Theft:** Web servers often store TLS private keys, SSH keys, or JWT signing keys on disk. A
path traversal vulnerability can expose these keys, enabling an attacker to impersonate
the server, decrypt captured traffic, or forge authentication tokens.

- **Denial of Service via Large File Reads:** An attacker can cause the server to read and stream large files (disk images, log files,
binary blobs), consuming server memory and network bandwidth and degrading availability
for legitimate users.


## FAQ

**Q: Is every send_from_directory() call vulnerable to path traversal?**

No. If the filename is a hardcoded string or a server-generated UUID with no user input,
there is no path traversal risk. This is an audit-grade rule -- it flags every call for
review to verify the filename source. Only calls where the filename comes from
request.args, request.form, URL path parameters, or other user-controlled sources are
actually vulnerable.


**Q: Does send_from_directory() protect against path traversal on its own?**

send_from_directory() joins the directory and filename using werkzeug's safe_join()
internally, which does provide some protection. However, if the filename contains
URL-encoded traversal sequences (%2e%2e%2f) that are decoded before reaching
safe_join(), or if the filename is joined with other user-controlled path segments
before being passed, traversal is still possible. Always apply secure_filename() first.


**Q: What does secure_filename() actually do?**

werkzeug.utils.secure_filename() strips all directory components (/ and \), removes
leading dots, and replaces dangerous characters with underscores. The result is a
filename that is safe to use as a file system path component. Note that it does not
validate that the file exists or that the caller has permission to access it -- those
checks are separate.


**Q: What happens if secure_filename() returns an empty string?**

secure_filename() returns an empty string if the input contains no safe characters.
Always check for an empty result and abort with a 400 error. Passing an empty string
to send_from_directory() will raise an error, but it is better to handle this explicitly.


**Q: Should I use send_file() or send_from_directory()?**

Prefer send_from_directory() because it always takes a directory and filename as
separate arguments and uses safe_join() internally. send_file() accepts an absolute
path string, which is more dangerous if any part of the path is user-controlled.


**Q: How do I run this rule in CI/CD?**

Run: pathfinder ci --ruleset python/flask/PYTHON-FLASK-SEC-017 --project .
The rule outputs SARIF, JSON, or CSV and can post inline pull request comments on GitHub.


**Q: What is the best architecture for serving user-uploaded files?**

Store user-uploaded files in object storage (S3, GCS, Azure Blob) and serve them via
pre-signed URLs. This removes the Flask application entirely from the file-serving path,
eliminates path traversal risk, and scales independently of the application server.
If local storage is required, serve files through a dedicated nginx location block
with the X-Accel-Redirect pattern rather than through Flask application code.


## References

- [CWE-73: External Control of File Name or Path](https://cwe.mitre.org/data/definitions/73.html)
- [Flask send_from_directory Documentation](https://flask.palletsprojects.com/en/stable/api/#flask.send_from_directory)
- [Werkzeug secure_filename Documentation](https://werkzeug.palletsprojects.com/en/stable/utils/#werkzeug.utils.secure_filename)
- [OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)
- [OWASP A01:2021 Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html)

---

Source: https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-017
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
