SeniorDebuggingOccasionalNot answered yet
This avatar-upload handler trusts the request for both the file type and the destination path — find and fix the flaws.
An avatar upload endpoint accepts a file and stores it for later serving.
Constraints:
- keep the endpoint contract (a multipart upload returning the URL of the stored file);
- only PNG and JPEG may be accepted;
- the stored file must never be executed by the web server;
- do not add an external scanning service.
UPLOAD_DIR = "/var/www/html/uploads"
@app.post("/upload")
def upload():
f = request.files["file"]
if f.content_type not in ("image/png", "image/jpeg"):
abort(400)
dest = os.path.join(UPLOAD_DIR, f.filename)
f.save(dest)
return {"url": "/uploads/" + f.filename}
Find the vulnerabilities in this handler and fix them.
Three trust defects. The declared content type is client-set, so detect the type from the bytes. The received filename is joined onto the storage path, so generate the stored name. The directory sits in the web root, so move it out and serve without execution.
- ✗Fixing the type check but keeping the client-supplied filename
- ✗Leaving the storage directory inside the web root
- ✗Writing the file first and validating it afterwards
- →Why does a generated stored name remove the traversal question entirely?
- →What must the serving handler set so the browser never interprets the file?
Walkthrough
The handler has three defects, and each of them is trust placed in request data.
content_typearrives in the request, so any content can be declared an image. The type must be derived from the bytes.f.filenameis joined onto the storage path, so the name can direct the write outside the directory. The server generates the name instead.UPLOAD_DIRsits inside the web root, so the stored file is served directly and may be handled as executable. Move the directory outside it and serve through a dedicated handler.
UPLOAD_DIR = "/srv/uploads" # outside the web root
EXT = {"png": ".png", "jpeg": ".jpg"}
@app.post("/upload")
def upload():
f = request.files["file"]
kind = imghdr.what(f.stream) # type from the bytes, not the header
f.stream.seek(0)
if kind not in EXT:
abort(400)
name = uuid4().hex + EXT[kind] # server-generated name, client name ignored
f.save(os.path.join(UPLOAD_DIR, name))
return {"url": url_for("serve_upload", name=name)}
The serving handler resolves only the generated name, sets a fixed Content-Type with Content-Disposition: attachment, and adds X-Content-Type-Options: nosniff. Re-encoding the accepted image and capping the request body size are worthwhile additions.