Uploading Files in Flask
To accept a file upload, the HTML form must set enctype="multipart/form-data" and include an <input type="file" name="photo"> element; without this encoding, the browser sends only the filename as plain text rather than the binary file contents. On the Flask side, uploaded files are not part of request.form — they arrive in request.files, a dict-like MultiDict where request.files['photo'] returns a werkzeug.datastructures.FileStorage object exposing .filename, .stream, .content_type, and a .save(path) method that writes the uploaded content to disk. If no file was selected, request.files.get('photo') returns a FileStorage object with an empty string filename rather than None, so checking if file and file.filename: is the correct emptiness test.
Cricket analogy: Like a stadium's separate gate for equipment trucks versus the turnstile gates for fans, multipart/form-data is a distinct 'lane' from regular form encoding that lets binary file bytes travel alongside text fields in one request.
Configuring Upload Settings
Two Flask configuration values are central to safe uploads: app.config['MAX_CONTENT_LENGTH'] caps the total size of an incoming request body in bytes (for example 16 * 1024 * 1024 for 16 MB), and Flask automatically raises a 413 Request Entity Too Large error if the client exceeds it, before the whole payload is even fully buffered into memory. A separate UPLOAD_FOLDER config value conventionally stores the absolute filesystem path where accepted files get written, and it's good practice to create this directory outside of any path directly served as static content unless files are meant to be publicly downloadable immediately after upload.
Cricket analogy: Like the boundary rope defining a hard limit beyond which a ball is out of play, MAX_CONTENT_LENGTH sets a hard byte-size boundary beyond which Flask rejects the request outright with a 413 error.
import os
from flask import Flask, request, abort
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'pdf'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload():
file = request.files.get('document')
if not file or not file.filename:
abort(400, 'No file selected.')
if not allowed_file(file.filename):
abort(400, 'Unsupported file type.')
filename = secure_filename(file.filename)
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return {'status': 'ok', 'filename': filename}, 201
Validating File Types and Securing Filenames
A file's client-supplied .filename is untrusted input and must never be used directly as a filesystem path — a malicious client can send a filename like ../../etc/passwd to attempt directory traversal. Werkzeug's secure_filename() utility strips path separators, normalizes the string to ASCII, and removes dangerous characters, turning something like ../../etc/passwd into etc_passwd. Checking the file extension against an allow-list (not a deny-list) is the correct approach for restricting upload types, since deny-lists are trivially bypassed by unusual or double extensions; note that extension checking alone does not verify the actual file content matches the claimed type, so for security-sensitive uploads you may also want to inspect the file's magic bytes or re-encode images through a trusted library like Pillow.
Cricket analogy: Like a stadium turnstile checking a ticket against the officially issued barcode list rather than trusting whatever a fan claims verbally, secure_filename() sanitizes an untrusted client-supplied filename rather than trusting it as-is.
Always use an allow-list of permitted extensions ({'png', 'jpg', 'pdf'}) rather than a deny-list of forbidden ones — deny-lists miss dangerous types like .php5, .phtml, or extensionless files with executable content, and even then, extension checking should be paired with content-based validation for anything security-sensitive.
Saving and Serving Uploaded Files
After sanitizing the filename with secure_filename(), it's good practice to also generate a unique name (for example prefixing with uuid.uuid4().hex) to avoid overwriting an existing file when two users upload files with the same name, and to store the original filename separately in a database record if you need to display it later. To serve uploaded files back to users, avoid placing the UPLOAD_FOLDER inside Flask's static/ directory if uploads should be access-controlled; instead, create a dedicated route like @app.route('/uploads/<filename>') that uses send_from_directory(app.config['UPLOAD_FOLDER'], filename), which lets you add authentication or ownership checks before releasing a file, something a plain static file route cannot do.
Cricket analogy: Like assigning each player a unique jersey number to avoid two players sharing '18' on the same team sheet, prefixing filenames with a UUID prevents two uploads from colliding and overwriting each other.
send_from_directory() also protects against path traversal on the read side by resolving the requested filename safely against the base directory and returning a 404 if it would escape that directory, so it should always be preferred over manually constructing a file path with os.path.join() and send_file().
- File uploads require
enctype="multipart/form-data"on the form and arrive viarequest.files, notrequest.form. app.config['MAX_CONTENT_LENGTH']caps request body size and triggers an automatic 413 error when exceeded.werkzeug.utils.secure_filename()sanitizes untrusted client filenames to prevent directory traversal attacks.- Validate file types with an allow-list of extensions, and consider checking magic bytes for security-sensitive uploads.
- Generate unique filenames (e.g. with
uuid.uuid4()) to avoid overwriting existing files on collision. - Use a dedicated route with
send_from_directory()to serve uploads that need access control, rather than Flask's static folder. send_from_directory()protects against path traversal on reads, unlike manually joined paths passed tosend_file().
Practice what you learned
1. What HTML form attribute is required to enable binary file uploads?
2. Where do uploaded files appear in a Flask request object?
3. What does werkzeug's secure_filename() primarily protect against?
4. What HTTP status code does Flask return automatically when MAX_CONTENT_LENGTH is exceeded?
5. Why is an allow-list of file extensions preferred over a deny-list?
Was this page helpful?
You May Also Like
Flask Forms with WTForms
Use Flask-WTF and WTForms to define form fields declaratively, validate user input server-side, and protect submissions with CSRF tokens.
Static Files in Flask
Understand how Flask serves CSS, JavaScript, and images from the static folder, how to reference them safely with url_for, and how to prepare static assets for production.
Flash Messages
Use Flask's flash() system to show one-time notifications like success and error alerts to users after a redirect, using session storage under the hood.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics