Part 3 – Accepting Uploads (multipart/form-data)
Receive files via POST; save deterministically; return explicit results.
Goals
Implement a POST route to accept
multipart/form-datauploads.Save artifacts with predictable naming in a target directory.
What You’ll Build
/uploadroute that accepts one file field and persists it safely.
Sections
Route Contract: Validate presence of file → sanitize filename → save → return JSON summary.
Storage Layout: Where to store (
save_dir), naming patterns, collisions.Security Considerations: Don’t trust client filenames; avoid path traversal; size limits (conceptually).
Milestone: curl POST works; file appears on disk with expected name.
Troubleshooting: Missing
Content-Type, wrong form field name, empty files.
A natural progression from allowing file downloads from the web server is to allow uploads. When performing assessments, there can be conditions where you need to transfer a file or files from a host to the attacking machine. Knowing how to create an instance of a server that can accept file uploads can be a nice skill to possess. I'll attempt to show how this is done using Python and Flask.
In the interest of security, I'll define the the name of the file to be uploaded (UPLOAD_FILE), the directory where to store any uploaded files (UPLOAD_DIRECTORY) and the path to tack onto a url (ROUTE) . I've named these with rather pedestrian strings for the purpose of this example. If you ever need to do this in the wild, providing random names for any or all of these is easily accomplished.
Apart from the initialization, I'll need to call on a few modules to help create this server. Path from pathlib is used for file and directory handling. From Flask, import Flask, request, and abort. Flaskis the method used to create the web application instance. request allows one to handle HTTP requests, and abort will allow the stoppage of a request handling and return the error code for a bad HTTP request (400).
As a slight aside, the files property is an immutable dictionary-like object from the Werkzeug Library. It's dictionary-like due to the object handling multiple values for the same key. So, under the hood, you would have something like the following:
From a REPL:

If you wanted to accept all files, you could change file = ... return ... to something like this:
Exercises
Upload multiple file types; verify extensions and sizes.
Upload same filename twice; decide overwrite vs versioning.
What’s Next
Base64-encoded exfil via query/body for hostile transports.
Upload methods with curl
Curl Flag
Content-Type sent
Body Structure
Flask Accessor
Typical Use Case
-d "a=1&b=2"
application/x-www-form-urlencoded
a=1&b=2
request.form["a"], request.form["b"]
Classic HTML forms
-d '{"x":1}' -H "Content-Type: application/json"
application/json
Raw JSON string
request.json or request.get_json()
REST APIs, JSON payloads
-F "file=@payload.bin"
multipart/form-data; boundary=...
Multipart with boundaries, one part per field/file
Files: request.files["file"]Fields: request.form["field"]
Browser-style file uploads
--data-binary @file.bin
application/octet-stream (default)
Raw file bytes
request.data (raw bytes)
Upload raw binary (no form encoding)
-T payload.bin
application/octet-stream (default)
Raw file bytes
request.data (if POST/PUT handler)
PUT/POST raw file, often for APIs
Last updated