Khi cần cho người dùng tải file — hóa đơn PDF, báo cáo CSV, ảnh đã upload — Flask có sẵn send_file và send_from_directory để làm việc này gọn và an toàn. Dùng đúng hai hàm này, bạn vừa ép trình duyệt tải xuống thay vì mở file, vừa chặn được kiểu tấn công chèn ../ vào đường dẫn để đọc trộm file ngoài thư mục cho phép. Bài này áp dụng khi file nằm trong một thư mục cố định như uploads hoặc exports.
Vấn đề
Cách nhiều người hay viết là tự nối đường dẫn rồi tự trả response. Đoạn code dưới đây chạy được, nhưng có hai lỗi: tên file lấy từ URL được nối thẳng vào đường dẫn, và toàn bộ file bị đọc hết vào RAM trước khi trả về.
from flask import Flask, Responseapp = Flask(__name__)@app.route("/download/<path:filename>")def download(filename): path = f"uploads/{filename}" # nguy hiểm: ../ đọc trộm file khác with open(path, "rb") as f: return Response(f.read(), mimetype="application/octet-stream")
Chỉ cần gọi /download/../../config.py, kẻ xấu có thể thoát khỏi thư mục uploads và đọc file khác trên server. Ngoài ra, vì không đặt tên file tải xuống, trình duyệt có thể mở file trực tiếp trong tab thay vì tải về.
Giải pháp
Nếu tên file đến từ URL, dùng send_from_directory: hàm này tự dùng safe_join của Werkzeug, nên đường dẫn thoát khỏi thư mục cho phép sẽ bị trả 404 thay vì đọc file.
from flask import Flask, send_from_directoryapp = Flask(__name__)@app.route("/download/<path:filename>")def download(filename): return send_from_directory( "uploads", filename, as_attachment=True, download_name=filename, )
Tham số as_attachment=True ép trình duyệt tải file xuống, còn download_name đặt đúng tên file người dùng sẽ thấy (từ Flask 2.2 trở đi, tham số cũ attachment_filename đã đổi tên thành download_name).
Còn file được tạo ra trong lúc chạy — ví dụ xuất báo cáo CSV từ database — hãy dùng send_file với một đối tượng file trong bộ nhớ:
import iofrom flask import Flask, send_fileapp = Flask(__name__)@app.route("/export")def export(): data = "name,total\nAn,120000\nBinh,85000\n" buffer = io.BytesIO(data.encode("utf-8")) buffer.seek(0) return send_file( buffer, mimetype="text/csv", as_attachment=True, download_name="bao-cao.csv", )
Quy tắc chọn rất đơn giản: file đã có sẵn trong một thư mục thì dùng send_from_directory, file tạo ra trong lúc chạy thì dùng send_file. Cả hai đều stream dữ liệu thay vì đọc hết vào RAM.
Áp dụng vào dự án của bạn
Trong dự án thật, đừng để thư mục chứa file tải xuống nằm trong static rồi cho truy cập tự do — ai đoán được tên file cũng tải được. Hãy đặt file ở thư mục riêng ngoài static, kiểm tra quyền sở hữu trước khi gọi send_from_directory, rồi ghi log mỗi lượt tải để đối soát. Với file xuất theo yêu cầu, đặt tên file kèm ngày như bao-cao-2026-10-27.csv để không bị ghi đè file cũ.
Bài viết liên quan
- Flask: dùng Application Factory (create_app) để app dễ test, dễ mở rộng ngay từ đầu
- Flask: tổ chức route gọn gàng bằng Blueprint khi dự án lớn dần
- FastAPI: cho phép client tải file an toàn bằng FileResponse, chặn path traversal trong 5 phút
Câu hỏi thường gặp
send_file và send_from_directory khác nhau thế nào?
send_file nhận một file đã xác định (đường dẫn tuyệt đối hoặc đối tượng file). send_from_directory nhận thêm thư mục gốc và tên file, tự nối đường dẫn an toàn và chặn path traversal. Khi tên file đến từ người dùng, luôn dùng send_from_directory.
Có cần tự kiểm tra ../ trong tên file nữa không?
Không, nếu bạn dùng send_from_directory thì Werkzeug đã chặn sẵn. Tuy nhiên bạn vẫn nên kiểm tra thêm: chỉ cho phép đuôi file mình hỗ trợ, và kiểm tra quyền truy cập trước khi trả file, vì chặn traversal không đồng nghĩa với phân quyền.
File rất lớn thì send_file có đọc hết vào RAM không?
Không, nếu bạn truyền đường dẫn hoặc đối tượng file cho send_file, Flask và Werkzeug sẽ stream từng phần thay vì đọc hết một lần. Điều cần tránh là tự .read() toàn bộ file rồi đưa chuỗi bytes vào response, như đoạn code lỗi ở đầu bài.
Tên file tiếng Việt có dấu có tải xuống đúng không?
Có. download_name được Werkzeug mã hóa theo chuẩn RFC 5987 nên trình duyệt hiện đại hiển thị đúng tên có dấu. Bạn không cần tự đặt header Content-Disposition thủ công.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
Nguồn tham khảo
- Flask documentation — send_file và send_from_directory: https://flask.palletsprojects.com/en/stable/api/#flask.send_file
- Werkzeug documentation — safe_join: https://werkzeug.palletsprojects.com/en/stable/utils/#werkzeug.utils.safe_join
