Upload files (tải lên file) trong ASP.NET Core
Nguồn: Upload files
Tổng quan
ASP.NET Core hỗ trợ upload một hoặc nhiều file sử dụng buffered model binding (ràng buộc mô hình có bộ đệm) cho các file nhỏ hơn và unbuffered streaming (truyền phát không có bộ đệm) cho các file lớn hơn.
Các vấn đề bảo mật
Khi cung cấp cho người dùng khả năng upload file lên server, hãy thận trọng với:
- Tấn công từ chối dịch vụ (Denial of service attacks)
- Upload virus/malware (phần mềm độc hại)
- Xâm phạm mạng và server
Các bước bảo mật quan trọng
- Upload file vào khu vực dành riêng cho upload file, tốt nhất là trên ổ đĩa không phải ổ hệ thống
- Không lưu các file đã upload trong cùng cây thư mục với ứng dụng
- Sử dụng tên file an toàn do ứng dụng xác định, không phải tên do người dùng cung cấp
- HTML encode (mã hóa HTML) tên file không đáng tin khi hiển thị chúng
- Chỉ cho phép các phần mở rộng file đã được chấp thuận
- Xác minh rằng các kiểm tra phía client được thực hiện trên server
- Kiểm tra kích thước file và đặt giới hạn kích thước tối đa
- Kiểm tra tên file với cơ sở dữ liệu hoặc bộ nhớ vật lý trước khi upload
- Chạy trình quét virus/malware trên nội dung đã upload trước khi lưu trữ
Các kịch bản lưu trữ
Cơ sở dữ liệu
- Nhanh hơn cho các file nhỏ
- Tiện hơn khi truy xuất cùng với dữ liệu người dùng
- Có thể tốn kém hơn so với lưu trữ đám mây
Lưu trữ vật lý (File System hoặc Network Share)
- Tốt hơn cho các file lớn
- Giới hạn cơ sở dữ liệu có thể hạn chế kích thước upload
- Có thể ít tốn kém hơn lưu trữ đám mây
- Không bao giờ được cấp quyền thực thi
Lưu trữ dữ liệu đám mây (ví dụ: Azure Blob Storage)
- Cải thiện khả năng mở rộng và độ bền
- Chi phí thấp hơn cho cơ sở hạ tầng lưu trữ lớn
Các kịch bản upload file
Buffering và Streaming
Buffering (có bộ đệm): Toàn bộ file được đọc vào IFormFile. Tốt cho các file nhỏ nhưng sử dụng tài nguyên đĩa và bộ nhớ.
Streaming (truyền phát): File được nhận từ multipart request và được xử lý trực tiếp. Giảm nhu cầu về bộ nhớ và không gian đĩa.
Các giá trị mặc định quan trọng
- MultipartBodyLengthLimit: 128 MB (kích thước tối đa mặc định cho các file form có bộ đệm)
- MemoryBufferThreshold: 64 KB (ngưỡng trước khi chuyển sang bộ đệm đĩa)
Upload file nhỏ với Buffered Model Binding vào Physical Storage
Ví dụ form cơ bản
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file" />
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>Ví dụ JavaScript/Fetch API
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>Upload nhiều file
Thêm thuộc tính multiple để hỗ trợ upload nhiều file:
<input asp-for="FileUpload.FormFiles" type="file" multiple />
Triển khai action method
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Xử lý các file đã upload
// Không dựa vào hoặc tin tưởng thuộc tính FileName mà không có xác thực.
return Ok(new { count = files.Count, size });
}Tạo tên file ngẫu nhiên
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}Xóa đường dẫn khỏi tên file
string untrustedFileName = Path.GetFileName(pathName);
⚠️ Cảnh báo: Không sử dụng thuộc tính FileName của IFormFile ngoại trừ để hiển thị và ghi log. Luôn HTML encode tên file.
Upload file nhỏ với Buffered Model Binding vào Database
Entity model
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}Page model
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}Upload handler
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload file nếu nhỏ hơn 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}⚠️ Cảnh báo: Hãy thận trọng khi lưu trữ dữ liệu nhị phân trong cơ sở dữ liệu quan hệ, vì điều này có thể ảnh hưởng xấu đến hiệu suất.
Upload file lớn với Streaming (truyền phát)
Sử dụng MultipartReader
// Đọc boundary từ header Content-Type
var boundary = HeaderUtilities.RemoveQuotes(
MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value;
// Sử dụng MultipartReader để truyền phát dữ liệu đến đích
var reader = new MultipartReader(boundary, Request.Body);
MultipartSection? section;
while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null)
{
var contentDisposition = section.GetContentDispositionHeader();
if (contentDisposition != null && contentDisposition.IsFileDisposition())
{
await section.Body.CopyToAsync(outputStream, cancellationToken);
}
}Sử dụng IFormFeature
// Lấy IFormFeature và đọc form
var formFeature = Request.HttpContext.Features.GetRequiredFeature<IFormFeature>();
await formFeature.ReadFormAsync(cancellationToken);
// Truy cập file đã upload (ví dụ: file đầu tiên)
var filePath = Request.Form.Files.First().FileName;
return Results.Ok($"Saved file at {filePath}");Custom Filter Attributes (thuộc tính bộ lọc tùy chỉnh)
Tạo Antiforgery Token Cookie
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Gửi request token như một cookie có thể đọc bằng JavaScript
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutingContext context)
{
}
}Vô hiệu hóa Form Value Model Binding
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}Đăng ký Filters (Cấu hình Startup)
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});Validation (Xác thực)
⚠️ Cảnh báo: Các phương thức xử lý validation không quét nội dung file. Luôn sử dụng trình quét virus/malware của bên thứ ba.
Validation nội dung
Sử dụng API quét virus/malware của bên thứ ba trên nội dung đã upload.
Việc quét đòi hỏi nhiều tài nguyên server. Hãy xem xét chuyển sang dịch vụ nền, có thể trên một server khác. Thông thường:
- Các file đã upload được giữ trong khu vực kiểm dịch (quarantine)
- Trình quét virus nền kiểm tra file
- File đạt hoặc không đạt dựa trên kết quả
- File được chấp thuận chuyển vào bộ nhớ thông thường
- Bản ghi cơ sở dữ liệu theo dõi trạng thái quét
Validation phần mở rộng file
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// Phần mở rộng không hợp lệ ... dừng xử lý file
}Validation chữ ký file
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}Bảo mật tên file
Không bao giờ sử dụng tên file do client cung cấp để lưu. Tạo tên file an toàn:
var trustedFileNameForFileStorage = Path.GetRandomFileName();
Razor tự động HTML encode các giá trị thuộc tính:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}Bên ngoài Razor, luôn HTML encode:
WebUtility.HtmlEncode(fileNameFromUser)
Kiểm tra xung đột tên file trước khi upload.
Validation kích thước
Giới hạn kích thước file đã upload. Cấu hình ví dụ:
{
"FileSizeLimit": 2097152
}Inject vào page models:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}Xác thực trong action method:
if (formFile.Length > _fileSizeLimit)
{
// File quá lớn ... dừng xử lý file
}Khớp tên thuộc tính với tên tham số
Tên phần tử form phải khớp với tên tham số/thuộc tính C#.
Ví dụ HTML:
<input type="file" name="battlePlans" multiple>
Ví dụ JavaScript:
var formData = new FormData();
for (var file in files) {
formData.append("battlePlans", file, file.name);
}Razor Pages handler:
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
MVC controller action:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
Cấu hình Server và App
Giới hạn Multipart Body Length
MultipartBodyLengthLimit đặt giới hạn cho mỗi phần thân multipart. Mặc định: 134,217,728 (128 MB).
Cấu hình trong Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Đặt giới hạn thành 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}Áp dụng cho các trang/action cụ thể:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Đặt giới hạn thành 256 MB
MultipartBodyLengthLimit = 268435456
});
});
});Hoặc trang trí class/method:
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}Kích thước Request Body tối đa của Kestrel
Mặc định: 30,000,000 bytes (~28.6 MB).
Cấu hình trong Program.cs hoặc Startup.ConfigureWebHost:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Xử lý các request lên đến 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});Áp dụng cho các trang/action cụ thể:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Xử lý các request lên đến 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});Hoặc trang trí class/method:
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}IIS
Giới hạn request mặc định (maxAllowedContentLength): 30,000,000 bytes (~28.6 MB).
Cấu hình trong web.config:
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>Hoặc trong Startup.ConfigureServices:
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});Xử lý sự cố
Lỗi Not Found khi Deploy lên IIS
HTTP 404.13 - Not Found The request filtering module is configured to deny a request that exceeds the request content length.
Giải pháp: Tăng maxAllowedContentLength trong web.config (xem phần IIS ở trên).
Lỗi kết nối
Lỗi kết nối và server reset có thể chỉ ra rằng file được upload vượt quá kích thước request body tối đa của Kestrel.
Giải pháp: Tăng MaxRequestBodySize trong các tùy chọn Kestrel (xem phần Kestrel ở trên). Cũng có thể cần điều chỉnh các giới hạn kết nối client của Kestrel.
Null Reference Exception với IFormFile
Nếu controller nhận được null cho tham số IFormFile:
- Xác nhận HTML form có
enctype="multipart/form-data" - Xác nhận tên phần tử form khớp với tên tham số/thuộc tính C#
- Xác nhận trường form được đặt tên đúng cách
Stream Was Too Long
MemoryStream có giới hạn kích thước là int.MaxValue. Đối với các file lớn hơn 50 MB, hãy sử dụng cách tiếp cận thay thế không phụ thuộc vào một MemoryStream duy nhất.
Path.GetTempFileName Throws IOException
Trong .NET 7 hoặc cũ hơn, Path.GetTempFileName ném IOException khi hơn 65,535 file được tạo mà không xóa các file tạm thời trước đó. Đây là giới hạn OS theo từng server.
Giải pháp: Xóa các file tạm thời sau khi xử lý hoặc sử dụng chiến lược đặt tên file khác.