Nguon: Microsoft Learn · .NET 8.0

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:

Các bước bảo mật quan trọng

Các kịch bản lưu trữ

Cơ sở dữ liệu

Lưu trữ vật lý (File System hoặc Network Share)

Lưu trữ dữ liệu đám mây (ví dụ: Azure Blob Storage)

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

Upload file nhỏ với Buffered Model Binding vào Physical Storage

Ví dụ form cơ bản

cshtml
<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

cshtml
<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:

cshtml
<input asp-for="FileUpload.FormFiles" type="file" multiple />

Triển khai action method

csharp
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

csharp
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

csharp
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

csharp
public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}

Page model

csharp
public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}

Upload handler

csharp
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

csharp
// Đọ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

csharp
// 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)

csharp
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

csharp
[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)

csharp
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:

  1. Các file đã upload được giữ trong khu vực kiểm dịch (quarantine)
  2. Trình quét virus nền kiểm tra file
  3. File đạt hoặc không đạt dựa trên kết quả
  4. File được chấp thuận chuyển vào bộ nhớ thông thường
  5. Bản ghi cơ sở dữ liệu theo dõi trạng thái quét

Validation phần mở rộng file

csharp
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

csharp
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:

csharp
var trustedFileNameForFileStorage = Path.GetRandomFileName();

Razor tự động HTML encode các giá trị thuộc tính:

cshtml
@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}

Bên ngoài Razor, luôn HTML encode:

csharp
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ụ:

json
{
  "FileSizeLimit": 2097152
}

Inject vào page models:

csharp
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}

Xác thực trong action method:

csharp
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:

html
<input type="file" name="battlePlans" multiple>

Ví dụ JavaScript:

javascript
var formData = new FormData();

for (var file in files) {
  formData.append("battlePlans", file, file.name);
}

Razor Pages handler:

csharp
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)

MVC controller action:

csharp
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:

csharp
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ể:

csharp
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:

csharp
[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:

csharp
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ể:

csharp
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:

csharp
[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:

xml
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

Hoặc trong Startup.ConfigureServices:

csharp
services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 52428800;
});

Xử lý sự cố

Lỗi Not Found khi Deploy lên IIS

code
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:

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.