Việc tải lên tệp an toàn trong ứng dụng web cần quyền truy cập chặt chẽ, giới hạn kích thước, URL ký tạm thời và mẫu quét mã độc đơn giản để tránh sự cố.

Tải lên tệp trông vô hại: ảnh đại diện, PDF, bảng tính. Nhưng chúng thường là nguồn gây ra sự cố bảo mật đầu tiên vì cho phép người lạ gửi vào hệ thống của bạn một “hộp bí ẩn”. Nếu bạn chấp nhận, lưu và hiển thị lại cho người khác, bạn đã tạo ra một đường tấn công mới cho ứng dụng.
Rủi ro không chỉ là “ai đó tải lên virus.” Một tệp xấu có thể làm lộ tài liệu riêng tư, làm tăng hóa đơn lưu trữ, hoặc lừa người dùng trao quyền truy cập. Một tệp tên là “invoice.pdf” có thể không phải PDF. Ngay cả PDF và ảnh thật cũng gây vấn đề nếu ứng dụng tin metadata, tự động tạo preview, hoặc phục vụ chúng với quy tắc sai.
Các sự cố thực tế thường trông như sau:
Một chi tiết gây ra nhiều sự cố: lưu trữ tệp không giống phục vụ tệp. Lưu trữ là nơi bạn giữ byte. Phục vụ là cách các byte đó được giao tới trình duyệt và app. Mọi chuyện đi sai khi app phục vụ tệp người dùng với cùng mức độ tin cậy và quy tắc như trang chính, khiến trình duyệt coi tệp tải lên là “đáng tin”.
“Mức độ an toàn đủ” cho một app nhỏ hoặc đang phát triển thường có nghĩa là bạn có thể trả lời bốn câu hỏi mà không vòng vo: ai có thể tải lên, bạn chấp nhận gì, kích thước và tần suất bao nhiêu, ai có thể đọc nó sau này. Ngay cả khi bạn xây dựng nhanh (với mã sinh tự động hoặc nền tảng chat-driven), các rào chắn đó vẫn quan trọng.
Hãy coi mỗi tệp tải lên như input không đáng tin. Cách thực tế để giữ an toàn là hình dung ai có thể lạm dụng chúng và “thành công” với kẻ tấn công trông như thế nào.
Hầu hết kẻ tấn công là bot quét các form upload yếu hoặc người dùng thật thử vượt giới hạn để lấy lưu trữ miễn phí, cào dữ liệu, hoặc phá hoại dịch vụ. Đôi khi là đối thủ tìm lỗ hổng rò rỉ hay gây outage.
Họ muốn gì? Thường là một trong các kết quả sau:
Rồi bản đồ các điểm yếu. Endpoint upload là cửa trước (tệp quá khổ, định dạng lạ, tần suất yêu cầu cao). Lưu trữ là phòng sau (bucket công khai, quyền sai, thư mục chia sẻ). URL tải xuống là lối ra (dễ đoán, sống lâu, hoặc không ràng buộc với người dùng).
Ví dụ: tính năng “tải lên hồ sơ xin việc”. Một bot tải hàng nghìn PDF lớn để làm tăng chi phí, trong khi một user lạm dụng tải tệp HTML và chia sẻ nó như “tài liệu” để lừa người khác.
Trước khi thêm kiểm soát, quyết định điều gì quan trọng nhất cho app của bạn: riêng tư (ai có thể đọc), khả năng phục vụ (có tiếp tục phục vụ được không), chi phí (lưu trữ và băng thông), và tuân thủ (dữ liệu lưu ở đâu và giữ bao lâu). Danh sách ưu tiên đó giúp các quyết định nhất quán.
Hầu hết sự cố upload không phải là hack cầu kỳ. Chúng là lỗi đơn giản “tôi nhìn thấy tệp của người khác”. Hãy coi quyền truy cập là một phần của quá trình upload, không phải tính năng thêm sau.
Bắt đầu với một quy tắc: mặc định từ chối. Giả định mọi đối tượng tải lên là riêng tư cho đến khi bạn cho phép rõ ràng. “Riêng tư theo mặc định” là nền tảng mạnh cho hóa đơn, hồ sơ y tế, tài liệu tài khoản và bất cứ thứ gì liên quan đến user. Chỉ cho phép công khai khi user rõ ràng mong đợi điều đó (ví dụ avatar công khai), và ngay cả khi đó cũng cân nhắc truy cập có thời hạn.
Giữ vai trò đơn giản và tách biệt. Một tách thường dùng là:
Đừng dựa vào quy tắc theo thư mục như “bất cứ thứ gì trong /user-uploads/ là ổn.” Kiểm tra quyền sở hữu hoặc quyền tenant khi đọc, cho mọi tệp. Điều đó bảo vệ bạn khi ai đó chuyển team, rời tổ chức, hoặc tệp bị gán lại.
Mô hình hỗ trợ tốt là hẹp và tạm thời: cấp quyền cho một tệp cụ thể, ghi nhật ký, và tự hết hạn.
Hầu hết cuộc tấn công bắt đầu bằng mẹo đơn giản: tệp trông an toàn vì tên hoặc header trình duyệt, nhưng thực ra là cái khác. Hãy đối xử mọi thứ client gửi là không đáng tin.
Bắt đầu bằng allowlist: quyết định chính xác định dạng bạn chấp nhận (ví dụ .jpg, .png, .pdf) và từ chối mọi thứ khác. Tránh “bất kỳ ảnh nào” hay “bất kỳ tài liệu nào” trừ khi bạn thực sự cần.
Đừng tin phần mở rộng tên tệp hay header Content-Type từ client. Cả hai đều dễ giả mạo. Một tệp tên invoice.pdf có thể là executable, và Content-Type: image/png có thể là lời nói dối.
Cách mạnh hơn là kiểm tra các byte đầu của tệp, thường gọi là “magic bytes” hoặc chữ ký tệp. Nhiều định dạng phổ biến có header nhất quán (như PNG và JPEG). Nếu header không khớp những gì bạn cho phép, từ chối.
Một thiết lập xác thực thực dụng:
Đổi tên quan trọng hơn bạn nghĩ. Nếu bạn lưu tên do người dùng cung cấp trực tiếp, bạn mời gọi trick về đường dẫn, ký tự lạ, và ghi đè vô ý. Dùng ID sinh để lưu và giữ tên gốc chỉ để hiển thị.
Với ảnh đại diện, chỉ chấp nhận JPEG và PNG, xác minh header và loại bỏ metadata nếu có thể. Với tài liệu, cân nhắc chỉ PDF và từ chối mọi thứ có nội dung tích cực. Nếu sau này bạn cần SVG hoặc HTML, coi chúng như có thể thực thi và cô lập chúng.
Hầu hết các sự cố upload không phải là “kỹ thuật hack”. Chúng là do tệp quá lớn, quá nhiều request, hoặc kết nối chậm chiếm máy chủ cho đến khi app cảm thấy sập. Hãy coi mỗi byte là một chi phí.
Chọn kích thước tối đa theo tính năng, không phải một con số toàn cục. Avatar không cần cùng giới hạn với tài liệu thuế hoặc video ngắn. Đặt giới hạn nhỏ nhất vẫn hợp lý, rồi thêm luồng “upload lớn” riêng khi bạn thật sự cần.
Thực thi giới hạn ở nhiều nơi, vì client có thể giả mạo: trong logic app, ở web server hoặc reverse proxy, với timeout upload, và từ chối sớm khi kích thước khai báo quá lớn (trước khi đọc toàn bộ body).
Ví dụ cụ thể: avatar giới hạn 2 MB, PDF giới hạn 20 MB, và mọi thứ lớn hơn đòi hỏi flow khác (ví dụ direct-to-object-storage với signed URL).
Ngay cả tệp nhỏ cũng có thể gây DoS nếu ai đó upload chúng liên tục. Thêm giới hạn tốc độ cho endpoint upload theo user và theo IP. Cân nhắc giới hạn chặt hơn cho traffic ẩn danh so với user đã đăng nhập.
Upload có thể nối tiếp (resumable) giúp người dùng thật trên mạng yếu, nhưng token session phải chặt: thời hạn ngắn, ràng buộc với user, và gắn với kích thước/tên tệp cụ thể. Nếu không, endpoint “resume” trở thành ống dẫn miễn phí vào lưu trữ của bạn.
Khi bạn chặn upload, trả lỗi rõ cho người dùng (tệp quá lớn, quá nhiều yêu cầu) nhưng đừng lộ nội bộ (stack trace, tên bucket, thông tin vendor).
Tải lên an toàn không chỉ là bạn chấp nhận gì. Nó còn là tệp đi đâu và bạn trả lại nó như thế nào sau này.
Giữ byte tải lên ngoài database chính của bạn. Hầu hết app chỉ cần metadata trong DB (owner user ID, tên gốc, loại phát hiện, kích thước, checksum, storage key, thời gian tạo). Lưu byte ở object storage hoặc dịch vụ file được thiết kế cho blob lớn.
Tách biệt tệp công khai và riêng tư ở cấp lưu trữ. Dùng các bucket hoặc container khác nhau với quy tắc khác nhau. Tệp công khai (như avatar công khai) có thể đọc mà không cần login. Tệp riêng tư (hợp đồng, hóa đơn, hồ sơ y tế) không bao giờ được công khai đọc, ngay cả khi ai đó đoán URL.
Tránh phục vụ tệp người dùng từ cùng domain với app chính khi có thể. Nếu một tệp rủi ro lọt qua (HTML, SVG có script, hoặc trình duyệt sniff MIME lạ), host nó trên domain chính có thể dẫn đến chiếm đoạt tài khoản. Một domain tải xuống riêng (hoặc domain lưu trữ) giới hạn phạm vi thiệt hại.
Khi tải xuống, giữ header an toàn. Đặt Content-Type dựa trên những gì bạn cho phép, không phải do user khai báo. Với bất cứ thứ gì trình duyệt có thể diễn giải, ưu tiên gửi như tệp tải xuống.
Một vài mặc định ngăn ngừa bất ngờ:
Content-Disposition: attachment cho tài liệu.Content-Type an toàn (hoặc application/octet-stream).Retention cũng là an ninh. Xóa các upload bị bỏ rơi, loại bỏ phiên bản cũ sau khi thay thế, và đặt giới hạn thời gian cho tệp tạm. Dữ liệu ít hơn nghĩa là ít thứ có thể bị rò rỉ.
Signed URLs (thường gọi pre-signed URLs) là cách phổ biến để cho phép người dùng upload hoặc download mà không làm bucket công khai, và không cần truyền mọi byte qua API của bạn. URL mang quyền tạm thời, rồi hết hạn.
Hai flow phổ biến:
Direct-to-storage giảm tải API, nhưng làm cho quy tắc lưu trữ và ràng buộc URL quan trọng hơn.
Hãy coi signed URL như một chìa khóa dùng một lần. Làm nó cụ thể và thời hạn ngắn.
Một mẫu thực tế là tạo bản ghi upload trước (status: pending), rồi phát hành signed URL. Sau khi upload, xác nhận object tồn tại và khớp kích thước/loại mong đợi trước khi đánh dấu là sẵn sàng.
Một flow upload an toàn chủ yếu là quy tắc rõ ràng và trạng thái rõ ràng. Coi mọi upload là không đáng tin cho tới khi kiểm tra xong.
Viết ra mỗi tính năng cho phép gì. Ảnh đại diện và tài liệu thuế không nên dùng chung loại tệp, giới hạn kích thước, hoặc quyền hiển thị.
Xác định loại cho phép và giới hạn kích thước theo tính năng (ví dụ: ảnh tới 5 MB; PDF tới 20 MB). Thực thi cùng quy tắc ở backend.
Tạo “bản ghi upload” trước khi byte đến. Lưu: owner (user hoặc org), mục đích (avatar, invoice, attachment), tên gốc, kích thước tối đa mong đợi, và trạng thái như pending.
Upload vào vị trí riêng tư. Đừng để client chọn đường dẫn cuối cùng.
Xác thực lại ở server: kích thước, magic bytes/loại, allowlist. Nếu qua, chuyển trạng thái sang uploaded.
Quét mã độc và cập nhật trạng thái sang clean hoặc quarantined. Nếu quét bất đồng bộ, giữ quyền truy cập khóa trong khi chờ.
Cho phép tải xuống, xem trước, hoặc xử lý chỉ khi trạng thái là clean.
Ví dụ nhỏ: với ảnh đại diện, tạo bản ghi liên kết với user và mục đích avatar, lưu riêng tư, xác nhận thật sự là JPEG/PNG (không chỉ tên giống), quét, rồi tạo URL preview.
Quét là một mạng lưới an toàn, không phải lời hứa. Nó bắt được tệp xấu đã biết và chiêu trò rõ ràng, nhưng không phát hiện hết mọi thứ. Mục tiêu là giảm rủi ro và làm cho tệp chưa rõ rủi ro trở nên vô hại theo mặc định.
Mẫu đáng tin là cách ly trước. Lưu mọi upload mới vào vị trí riêng tư, đánh dấu là pending. Chỉ sau khi vượt kiểm tra mới chuyển sang vị trí “clean” (hoặc đánh dấu là sẵn sàng).
Quét đồng bộ chỉ hợp lý cho tệp nhỏ và traffic thấp vì user phải đợi. Hầu hết app quét bất đồng bộ: chấp nhận upload, trả trạng thái “đang xử lý”, quét ở background.
Quét cơ bản thường là engine antivirus (hoặc dịch vụ) cộng vài rào cản: AV scan, kiểm tra loại tệp (magic bytes), giới hạn archive (zip bombs, zip lồng nhau, kích thước giải nén khổng lồ), và chặn các định dạng bạn không cần.
Nếu scanner lỗi, timeout, hoặc trả “unknown”, coi tệp là đáng ngờ. Giữ nó cách ly và không cung cấp link tải xuống. Đây là nơi nhiều đội bị đau: “scan failed” không nên biến thành “vẫn cho dùng”.
Khi bạn chặn tệp, giữ thông báo trung tính: “Chúng tôi không thể chấp nhận tệp này. Thử tệp khác hoặc liên hệ hỗ trợ.” Đừng khẳng định bạn đã phát hiện mã độc trừ khi bạn chắc chắn.
Xem hai tính năng: ảnh đại diện (hiển thị công khai) và biên lai PDF (riêng tư, dùng cho thanh toán hoặc hỗ trợ). Cả hai đều là vấn đề upload, nhưng không nên có cùng quy tắc.
Với ảnh đại diện, giữ chặt: chỉ cho phép JPEG/PNG, giới hạn kích thước (ví dụ 2–5 MB), và mã hóa lại phía server để bạn không phục vụ byte gốc của người dùng. Lưu công khai chỉ sau khi kiểm tra.
Với biên lai PDF, chấp nhận kích thước lớn hơn (ví dụ tới 20 MB), giữ mặc định là riêng tư, và tránh render inline từ domain chính của bạn.
Mô hình trạng thái đơn giản giữ người dùng biết mà không lộ nội bộ:
Signed URLs phù hợp ở đây: dùng signed URL ngắn hạn cho upload (chỉ ghi, một object key). Phát hành signed URL đọc ngắn hạn riêng và chỉ khi trạng thái là clean.
Ghi lại những gì cần cho điều tra, không phải nội dung tệp: user ID, file ID, đoán loại, kích thước, storage key, timestamps, kết quả quét, request IDs. Tránh log nội dung thô hoặc dữ liệu nhạy bên trong tài liệu.
Hầu hết lỗi upload vì một shortcut “tạm thời” trở thành vĩnh viễn. Giả sử mọi tệp là không đáng tin, mọi URL sẽ được chia sẻ, và mọi cài đặt “sẽ sửa sau” sẽ bị quên.
Các bẫy lặp lại:
Content-Type sai, để trình duyệt diễn giải nội dung rủi ro.Giám sát là điều các đội bỏ qua cho tới khi hóa đơn lưu trữ tăng vọt. Theo dõi khối lượng upload, kích thước trung bình, người upload nhiều nhất, và tỷ lệ lỗi. Một tài khoản bị xâm có thể lặng lẽ upload hàng nghìn tệp lớn trong một đêm.
Ví dụ: một đội lưu avatar theo tên do user cung cấp như “avatar.png” trong thư mục chia sẻ. Một user ghi đè ảnh người khác. Cách sửa tẻ nhạt nhưng hiệu quả: sinh object key phía server, giữ upload mặc định riêng tư, và cung cấp ảnh resize thông qua phản hồi được kiểm soát.
Dùng đây như bước rà soát cuối trước khi phát hành. Xem mỗi mục là blocker phát hành, vì hầu hết sự cố đến từ một rào chắn bị thiếu.
Content-Type dự đoán được, tên tệp an toàn, và attachment cho tài liệu.Viết ra quy tắc bằng ngôn ngữ đơn giản: loại cho phép, kích thước tối đa, ai truy cập gì, signed URL sống bao lâu, và “scan passed” nghĩa là gì. Đó là hợp đồng chung giữa product, engineering và support.
Thêm vài test bắt lỗi thường gặp: tệp quá lớn, executable đổi tên, đọc trái phép, signed URL hết hạn, và tải xuống khi “scan pending”. Những test này rẻ so với một sự cố.
Nếu bạn đang xây dựng và lặp nhanh, hữu ích khi dùng workflow có thể lên kế hoạch và rollback an toàn. Các nhóm dùng Koder.ai (koder.ai) thường dựa vào planning mode và snapshots/rollback khi siết chặt quy tắc upload theo thời gian, nhưng yêu cầu cốt lõi vẫn vậy: backend phải thực thi chính sách, không phải UI.
Bắt đầu với mặc định là riêng tư và coi mọi tệp tải lên như dữ liệu không đáng tin. Thực thi bốn điều cơ bản ở phía server:
Nếu bạn trả lời được rõ ràng những điều đó, bạn đang an toàn hơn hầu hết các sự cố.
Bởi vì người dùng có thể tải lên một “hộp bí ẩn” mà ứng dụng của bạn lưu lại và có thể phục vụ lại cho người khác. Điều này có thể dẫn đến:
Hiếm khi chỉ là “ai đó tải lên một con virus”.
Lưu là đặt các byte ở đâu đó. Phục vụ là cách các byte đó được gửi đến trình duyệt và ứng dụng.
Nguy hiểm xảy ra khi ứng dụng phục vụ tệp người dùng với cùng mức độ tin cậy và quy tắc như trang chính của bạn. Nếu một tệp rủi ro bị xử lý như một trang bình thường, trình duyệt có thể thực thi nó (hoặc người dùng tin tưởng nó quá mức).
Một mặc định an toàn hơn là: lưu ở chế độ riêng tư, rồi phục vụ qua các phản hồi tải xuống được kiểm soát với header an toàn.
Sử dụng mặc định là từ chối và kiểm tra quyền mỗi lần một tệp được tải xuống hoặc xem trước.
Quy tắc thực tế:
Đừng tin phần mở rộng tên tệp hay Content-Type từ trình duyệt. Xác thực ở phía server:
Bởi vì nhiều sự cố là do lạm dụng nhàm chán: quá nhiều upload, tệp quá lớn, hoặc kết nối chậm làm chiếm tài nguyên máy chủ.
Các mặc định hữu ích:
Xem mỗi byte như chi phí và mỗi request như khả năng lạm dụng.
Có, nhưng phải cẩn trọng. Signed URLs cho phép trình duyệt upload/download trực tiếp tới object storage mà không làm bucket công khai.
Mặc định an toàn:
Direct-to-storage giúp giảm tải API, nhưng việc đặt phạm vi và thời hạn là bắt buộc.
Mẫu an toàn nhất là:
pendingQuét giúp giảm rủi ro, nhưng không bảo đảm. Dùng nó như một lớp an toàn bổ sung, không phải là biện pháp duy nhất.
Cách thực tế:
Chìa khóa là chính sách: “chưa quét” không bao giờ được hiểu là “sẵn sàng dùng”.
Phục vụ tệp theo cách ngăn trình duyệt diễn giải nó như trang web.
Mặc định tốt:
Content-Disposition: attachment cho tài liệuHầu hết lỗi thực tế là những sai sót đơn giản kiểu “tôi thấy được tệp của người khác”.
Nếu byte không khớp định dạng cho phép, từ chối việc tải lên.
cleanquarantinedcleanĐiều này ngăn tình trạng “quét thất bại” hoặc “đang xử lý” bị chia sẻ vô tình.
Content-Typeapplication/octet-streamĐiều này giảm nguy cơ một tệp tải lên biến thành trang lừa đảo hoặc thực thi script.