How it arises in Flask
send_file() with a path you built. flask.send_file(path_or_file) sends the contents of the file it is given. It is the right tool for a file your code chose. Views join an upload folder with a request value and pass the result in; send_file has no base directory to compare against.
The path converter. @app.route("/files/<path:name>") accepts slashes. Flask's own example for serving uploads uses exactly this route, together with send_from_directory().
A client-chosen directory. send_from_directory(directory, path) is documented as a secure way to serve files from a folder. The same documentation adds a condition: directory must not be a value provided by the client, otherwise it becomes insecure. Multi-tenant apps that build the directory from a tenant name in the URL break that condition.
Upload names. request.files["file"].filename is sent by the client. Flask's upload pattern says to always pass it through secure_filename() before joining it to the upload folder. Handlers that keep the original name "so users recognise their files" skip it.
Two join traps. os.path.join() discards everything before an absolute component. And werkzeug.security.safe_join() returns None when the path is unsafe; code that does not test for None fails later with a confusing error, and gets "fixed" by going back to os.path.join.
Vulnerable and fixed
import os
from flask import Flask, request, send_file
app = Flask(__name__)
app.config["UPLOAD_FOLDER"] = "/srv/app/uploads"
@app.get("/files/<path:name>")
def download(name):
return send_file(os.path.join(app.config["UPLOAD_FOLDER"], name))
@app.post("/files")
def upload():
f = request.files["file"]
f.save(os.path.join(app.config["UPLOAD_FOLDER"], f.filename))
return {"saved": f.filename}
send_from_directory() uses safe_join() internally and raises a 404 when the result leaves the directory or is not a regular file. secure_filename() reduces the upload name to ASCII letters, digits and ._-, and can return an empty string, which must be handled:
import os
from flask import Flask, abort, request, send_from_directory
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config["UPLOAD_FOLDER"] = "/srv/app/uploads"
@app.get("/files/<path:name>")
def download(name):
return send_from_directory(app.config["UPLOAD_FOLDER"], name, as_attachment=True)
@app.post("/files")
def upload():
f = request.files["file"]
name = secure_filename(f.filename or "")
if not name:
abort(400)
f.save(os.path.join(app.config["UPLOAD_FOLDER"], name))
return {"saved": name}
What Flask and Werkzeug already do for you
Most of the work, if you use them. send_from_directory() exists for this exact case and its documentation states the guarantee and its condition. safe_join() joins untrusted components to a trusted base and refuses to escape it. secure_filename() makes a name safe for a regular filesystem and for os.path.join(). The default string converter does not match slashes. Flask's upload guide shows all of these together in a dozen lines.
What es-python adds
Flask apps tend to grow file handling in several places: a download route, an export, a thumbnailer, an admin log viewer. es-python gives you the list from running traffic. Each time a request-supplied path reaches a file operation it records the base event, io open for open(), with the path and the view. When that path contains a .. segment or is absolute, a path traversal event fires beside it, tagged path traversal and graded Critical for network data. An embedded NUL byte is reported as null byte injection.
Used with the fixed code, the picture changes in a specific way. send_from_directory() refuses an escaping path before opening anything, so a probe yields a 404 and no path traversal event at an open. A legitimate download still shows an io open, at a lower grade, because the request did pick the file. EyalSec also recognises secure_filename(): an event for a path built from its output is still shown, with its severity capped.
Writes have their own treatment. An upload saved to disk is a disclosure question, so es-python records where the data was written and whether other users on the machine can read that file. For blocking, a Raise rule on path traversal raises a RuntimeError before the file is opened, and the Block backend network attacks template includes one.
Review checklist
- Search for
send_file(and confirm no argument derives from a request. - Use
send_from_directory()with a constant directory. - Call
secure_filename()on every upload name and handle the empty result. - Test
safe_join()results forNone. - Generate storage names yourself (a UUID) and keep the original name in the database.
References
- Reference: Flask documentation: API, send_from_directory() and send_file() (checked 2026-10-12)
- Reference: Flask documentation: Uploading Files (checked 2026-10-12)
- Reference: Werkzeug documentation: Utilities, secure_filename() and safe_join() (checked 2026-10-12)