How it arises in Django
Download views. Media that needs a permission check cannot be served by the web server directly, so a view does it: check the user, join MEDIA_ROOT with a name from the URL, open, return FileResponse. The check is about the user. Nothing checks the name.
The path converter. In a URLconf, <str:name> matches one segment. <path:name> matches any non-empty string including the path separator, so that a complete path can be captured. It is the right converter for nested media and it delivers slashes to the view by design.
os.path.join with an absolute component. If the second argument starts with /, the result is that argument alone: os.path.join("/srv/media", "/etc/passwd") is /etc/passwd. A view that strips .. and forgets this is still open.
django.views.static.serve outside development. Django documents that this view is not hardened for production use and should be used only as a development aid. It gets wired into production URLconfs to "temporarily" serve MEDIA_ROOT.
Upload names used directly. UploadedFile.name is the client's file name. Saving through a FileField or a storage's save() is safe; writing open(os.path.join(root, f.name), "wb") yourself is not.
Archive imports. Admin tools that accept a zip of fixtures or theme files and extract every member.
Vulnerable and fixed
URLconf entry: path("reports/<path:name>", views.report).
import os
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import FileResponse
@login_required
def report(request, name):
full = os.path.join(settings.MEDIA_ROOT, "reports", name)
return FileResponse(open(full, "rb"), as_attachment=True)
Resolve the final path, symlinks included, and require it to stay under the base directory. Better still, look the file up through a model so the request carries an id, not a path:
from pathlib import Path
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import FileResponse, Http404
@login_required
def report(request, name):
base = (Path(settings.MEDIA_ROOT) / "reports").resolve()
full = (base / name).resolve()
if not full.is_relative_to(base) or not full.is_file():
raise Http404("no such report")
return FileResponse(full.open("rb"), as_attachment=True)
What Django already does for you
The storage layer is careful. Storage.get_valid_name() returns a file name suitable for the target storage, generate_filename() validates it, and FileField.upload_to keeps uploads in a directory you chose. Django raises SuspiciousFileOperation when file operations look wrong. The str converter excludes slashes. And the documentation is explicit that static.serve is a development aid. Stay on those rails for uploads, and the remaining risk is custom read paths.
What es-python adds
es-python reports when a path that came from the request reaches a file operation. Two events fire together on a hit. The base event names the operation, io open for open(). Beside it, a path traversal event fires when the request-supplied path contains a .. segment or is absolute, and that one carries the path traversal impact tag and is graded Critical for data from the network.
That split is useful on a download view. Ordinary traffic produces io open events, showing that a request chooses the file at all, which is worth knowing by itself. A probe with ../ or a leading slash produces path traversal, and the event shows the exact value and the view. On the fixed view, a traversal attempt is rejected before any open(), so the sharper event does not appear.
Some limits, so you can read the events correctly. Matching is byte-level and POSIX-only: it looks for / separators and does not decode percent-encoding itself. Django decodes the URL before your view sees it, and the check runs on the decoded value your code actually passes to open(). For archive imports, the raw member name is checked before extraction and reported as zip slip or tar slip.
The Block backend network attacks template, or any Raise rule on path traversal, raises a RuntimeError before the file is opened.
Review checklist
- Search for
FileResponse(,open(andos.path.join(in views. - Check every
<path:...>converter and what its view does with the value. - Remove
django.views.static.servefrom production URLconfs. - Save uploads through
FileFieldor the storage API only. - Prefer database ids over file names in URLs.
References
- Reference: Django documentation: URL dispatcher, path converters (checked 2026-10-12)
- Reference: Django documentation: Built-in views, static.serve (checked 2026-10-12)
- Reference: Django documentation: The File storage API (checked 2026-10-12)
- Reference: Django documentation: Request and response objects, FileResponse (checked 2026-10-12)