Python Flask File Uploads: Handling and Saving User Files
python flask file uploads: Learn how to handle file uploads in Flask: accessing request.files, validating type and size, saving securely, and avoiding path traversal.
When a user submits a form with a file input, Flask exposes the uploaded file through request.files. The first step in handling python flask file uploads is understanding how this object works. request.files is a MultiDict containing FileStorage objects, each representing a file sent with the request. You access a file by its form field name, just like you would with request.form.
Accessing Uploaded Files with request.files
Consider a simple HTML form that lets a user upload a single file:
<form method="post" enctype="multipart/form-data"> <input type="file" name="document"> <input type="submit" value="Upload"> </form>
The enctype="multipart/form-data" is required for file uploads. In the Flask route, you retrieve the file like this:
from flask import Flask, request app = Flask(__name__) @app.route('/upload', methods=['POST']) def upload_file(): uploaded_file = request.files.get('document') if uploaded_file is None: return 'No file part', 400 # Do something with the file return 'File received', 200
The FileStorage object has a filename attribute that contains the original name from the client. It also has a save(destination) method that writes the file to a path or file-like object. The stream attribute gives you direct access to the underlying file stream, which is useful for reading or processing the data incrementally.
Saving the Uploaded File with secure_filename
The simplest way to save an uploaded file is to call save() with a path. However, using the client-provided filename directly is dangerous. An attacker can craft a filename like ../../etc/passwd to overwrite files outside your upload directory. Flask's werkzeug provides secure_filename() to sanitize the name:
from werkzeug.utils import secure_filename @app.route('/upload', methods=['POST']) def upload_file(): uploaded_file = request.files.get('document') if uploaded_file is None: return 'No file part', 400 safe_name = secure_filename(uploaded_file.filename) upload_path = os.path.join('/var/uploads', safe_name) uploaded_file.save(upload_path) return 'File saved', 200
secure_filename() strips path separators, removes control characters, and replaces spaces with underscores. It also converts the name to ASCII. For example, ../../photo of me.png becomes photo_of_me.png. This prevents path traversal and makes the filename safe to use in URLs.
Keep in mind that secure_filename() may return an empty string if the original filename contains only non-ASCII characters. In that case, you need to generate a fallback name, such as a UUID, to avoid errors.
Validating File Type and Size
Accepting any file without validation can lead to security issues and wasted storage. Two common checks are file type and file size. File type validation based on the extension is easy but unreliable because the client can set any filename. A more robust approach is to inspect the file's content using libraries like python-magic to detect the actual MIME type. For many applications, extension checking is sufficient, but you should be aware of its limitations.
ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg'} def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/upload', methods=['POST']) def upload_file(): uploaded_file = request.files.get('document') if uploaded_file is None: return 'No file part', 400 if not allowed_file(uploaded_file.filename): return 'File type not allowed', 400 # ... save
To limit file size, set MAX_CONTENT_LENGTH in the Flask config. This aborts the request with a 413 status if the total request body exceeds the limit:
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB
This limit applies to the entire request, not just the file. If you need per-file limits, you can check the file size after reading it, but that requires buffering the whole file. For large uploads, streaming to disk and checking the size as you write is more memory-efficient.
Preventing Path Traversal and Malicious Names
Even with secure_filename(), you should not trust the sanitized name blindly. Always combine it with a fixed upload directory and ensure the final path is within that directory. Use os.path.join and then verify the absolute path starts with the upload directory:
import os UPLOAD_FOLDER = '/var/uploads' @app.route('/upload', methods=['POST']) def upload_file(): uploaded_file = request.files.get('document') if uploaded_file is None: return 'No file part', 400 safe_name = secure_filename(uploaded_file.filename) if not safe_name: safe_name = str(uuid.uuid4()) upload_path = os.path.join(UPLOAD_FOLDER, safe_name) # Ensure the resolved path is inside UPLOAD_FOLDER if os.path.abspath(upload_path).startswith(os.path.abspath(UPLOAD_FOLDER)): uploaded_file.save(upload_path) return 'File saved', 200 else: return 'Invalid filename', 400
This double-check is important because secure_filename() is not a security boundary; it is a normalization step. The startswith check catches cases where a symlink or a crafted path could still escape the intended folder.
Streaming Large Uploads to Disk
When users upload large files, reading the entire file into memory before saving can exhaust server memory. The FileStorage object's stream attribute gives you a file-like object that you can read in chunks and write directly to disk:
@app.route('/upload', methods=['POST']) def upload_file(): uploaded_file = request.files.get('document') if uploaded_file is None: return 'No file part', 400 safe_name = secure_filename(uploaded_file.filename) upload_path = os.path.join(UPLOAD_FOLDER, safe_name) with open(upload_path, 'wb') as f: while True: chunk = uploaded_file.stream.read(1024 * 1024) if not chunk: break f.write(chunk) return 'File saved', 200
This approach keeps memory usage constant regardless of file size. It also lets you compute a hash or enforce a per-file size limit while writing. For example, you can count bytes and abort if the file exceeds a threshold, though the request body limit already prevents the whole request from being too large.
Handling Multiple Files in One Request
A form can include multiple file inputs with the same name. In that case, request.files.getlist('documents') returns a list of FileStorage objects. The HTML form would look like this:
<input type="file" name="documents" multiple>
And the Flask route processes each file:
@app.route('/upload', methods=['POST']) def upload_files(): files = request.files.getlist('documents') saved_names = [] for uploaded_file in files: safe_name = secure_filename(uploaded_file.filename) if not safe_name: safe_name = str(uuid.uuid4()) upload_path = os.path.join(UPLOAD_FOLDER, safe_name) uploaded_file.save(upload_path) saved_names.append(safe_name) return {'saved': saved_names}, 200
Each file is processed independently, so you can apply the same validation and security checks inside the loop. Be mindful of the total request size limit, which applies to all files combined.
Production Considerations for Storing and Serving Uploads
Saving files to the local filesystem is fine for small applications, but production deployments often need a different approach. If you run multiple app instances behind a load balancer, files saved on one instance are not available on another. Consider storing uploads in a shared object storage service like Amazon S3, Google Cloud Storage, or a dedicated file server. Flask's FileStorage.save() accepts a file-like object, so you can stream the file directly to an external service using its client library.
When serving uploaded files back to users, avoid using a static route that exposes the upload directory. Instead, use send_from_directory with proper access controls:
from flask import send_from_directory @app.route('/uploads/<path:filename>') def uploaded_file(filename): return send_from_directory(UPLOAD_FOLDER, filename)
This endpoint can be protected with authentication or authorization logic before sending the file. It also prevents path traversal because send_from_directory normalizes the path and raises a 404 if the file is outside the specified directory.
Finally, consider setting a request body size limit that matches your application's needs. A limit that is too low breaks legitimate uploads; one that is too high exposes you to denial-of-service attacks. Monitor disk usage and set up cleanup jobs for temporary or expired files. The exact values depend on your use case, but the mechanisms described here give you the control you need to handle python flask file uploads safely and efficiently.