Bagikan melalui


Mengunggah file di ASP.NET Core

Oleh Rutger Storm

ASP.NET Core mendukung pengunggahan satu atau beberapa file menggunakan pengikatan model buffer untuk file yang lebih kecil dan streaming yang tidak dibuffer untuk file yang lebih besar.

Melihat atau mengunduh kode sampel (cara mengunduh)

Pertimbangan keamanan

Berhati-hatilah saat memberi pengguna kemampuan untuk mengunggah file ke server. Cyberattackers dapat mencoba untuk:

  • Jalankan penolakan serangan layanan .
  • Unggah virus atau malware.
  • Membahayakan jaringan dan server dengan cara lain.

Langkah-langkah keamanan yang mengurangi kemungkinan serangan yang berhasil adalah:

  • Unggah file ke area unggahan file khusus, sebaiknya ke drive non-sistem. Lokasi khusus memudahkan untuk memberlakukan pembatasan keamanan pada file yang diunggah. Nonaktifkan izin eksekusi pada location.† unggahan file
  • Jangan menyimpan file yang diunggah di pohon direktori yang sama dengan app.†
  • Gunakan nama file aman yang ditentukan oleh aplikasi. Jangan gunakan nama file yang disediakan oleh pengguna atau nama file yang tidak tepercaya dari file yang diunggah.† HTML mengodekan nama file yang tidak tepercaya saat menampilkannya. Misalnya, mencatat nama file atau menampilkan di UI (Razor output pengodean HTML secara otomatis).
  • Izinkan hanya ekstensi file yang disetujui untuk spesifikasi desain aplikasi.†
  • Verifikasi bahwa pemeriksaan sisi klien dilakukan pada pemeriksaan sisi klien server.† mudah disinggung.
  • Periksa ukuran file yang diunggah. Atur batas ukuran maksimum untuk mencegah uploads.† besar
  • Ketika file tidak boleh ditimpa oleh file yang diunggah dengan nama yang sama, periksa nama file terhadap database atau penyimpanan fisik sebelum mengunggah file.
  • Jalankan pemindai virus/malware pada konten yang diunggah sebelum file disimpan.

† Aplikasi sampel menunjukkan pendekatan yang memenuhi kriteria.

Peringatan

Mengunggah kode berbahaya ke sistem sering kali merupakan langkah pertama untuk mengeksekusi kode yang dapat:

  • Sepenuhnya mendapatkan kendali atas sistem.
  • Membebani sistem secara berlebih dengan hasil yang dilumpuhkan sistem.
  • Membahayakan data pengguna atau sistem.
  • Terapkan grafiti ke UI publik.

Untuk informasi tentang mengurangi kerentanan saat menerima file dari pengguna, lihat sumber daya berikut:

Untuk informasi selengkapnya tentang menerapkan langkah-langkah keamanan, termasuk contoh dari aplikasi sampel, lihat bagian Validasi .

Skenario penyimpanan

Opsi penyimpanan umum untuk file meliputi:

  • Database

    • Untuk unggahan file kecil , database sering kali lebih cepat daripada opsi penyimpanan fisik (sistem file atau berbagi jaringan).
    • Database sering lebih nyaman daripada opsi penyimpanan fisik karena pengambilan rekaman database untuk data pengguna dapat secara bersamaan menyediakan konten file (misalnya, gambar avatar).
    • Database berpotensi lebih murah daripada menggunakan layanan penyimpanan data cloud.
  • Penyimpanan fisik (sistem file atau berbagi jaringan)

    • Untuk unggahan file besar:
      • Batas database dapat membatasi ukuran unggahan.
      • Penyimpanan fisik sering kali kurang ekonomis daripada penyimpanan dalam database.
    • Penyimpanan fisik berpotensi lebih murah daripada menggunakan layanan penyimpanan data cloud.
    • Proses aplikasi harus memiliki izin baca dan tulis ke lokasi penyimpanan. Jangan pernah memberikan izin eksekusi.
  • Layanan penyimpanan data cloud, misalnya, Azure Blob Storage.

    • Layanan biasanya menawarkan peningkatan skalabilitas dan ketahanan atas solusi lokal yang biasanya tunduk pada satu titik kegagalan.
    • Layanan berpotensi menurunkan biaya dalam skenario infrastruktur penyimpanan besar.

    Untuk informasi selengkapnya, lihat Mulai Cepat: Menggunakan .NET untuk membuat blob di penyimpanan objek.

File kecil dan besar

Definisi file kecil dan besar tergantung pada sumber daya komputasi yang tersedia. Aplikasi harus menjadi tolok ukur pendekatan penyimpanan yang digunakan untuk memastikannya dapat menangani ukuran yang diharapkan. Memori tolok ukur, CPU, disk, dan performa database.

Meskipun batas tertentu tidak dapat disediakan pada apa yang kecil versus besar untuk penyebaran Anda, berikut adalah beberapa default terkait ASP.NET Core untuk FormOptions (dokumentasi API):

  • Secara default, HttpRequest.Form tidak buffer seluruh isi permintaan (BufferBody), tetapi buffer file formulir multipart disertakan.
  • MultipartBodyLengthLimit adalah ukuran maksimum untuk file formulir buffer (default: 128 MB).
  • MemoryBufferThreshold menunjukkan ambang buffering dalam memori sebelum beralih ke file buffer pada disk (default: 64 KB). MemoryBufferThreshold bertindak sebagai batas antara file kecil dan besar, yang dinaikkan atau diturunkan tergantung pada sumber daya dan skenario aplikasi.

Untuk informasi selengkapnya tentang FormOptions, lihat FormOptions kelas di sumber referensi ASP.NET Core.

Catatan

Tautan dokumentasi ke sumber referensi .NET biasanya memuat cabang default repositori, yang mewakili pengembangan saat ini untuk rilis .NET berikutnya. Untuk memilih tag rilis tertentu, gunakan daftar dropdown Beralih cabang atau tag. Untuk informasi lebih lanjut, lihat Cara memilih tag versi kode sumber ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Skenario unggahan file

Dua pendekatan umum untuk mengunggah file adalah buffering dan streaming.

Buffering

Seluruh file dibaca ke dalam IFormFile. IFormFile adalah representasi C# dari file yang digunakan untuk memproses atau menyimpan file.

Disk dan memori yang digunakan oleh unggahan file bergantung pada jumlah dan ukuran unggahan file bersamaan. Jika aplikasi mencoba buffer terlalu banyak unggahan, situs mengalami crash saat kehabisan memori atau ruang disk. Jika ukuran atau frekuensi unggahan file melelahkan sumber daya aplikasi, gunakan streaming.

Setiap file buffer tunggal yang melebihi 64 KB dipindahkan dari memori ke file sementara pada disk.

File sementara untuk permintaan yang lebih besar ditulis ke lokasi bernama dalam ASPNETCORE_TEMP variabel lingkungan. Jika ASPNETCORE_TEMP tidak ditentukan, file ditulis ke folder sementara pengguna saat ini.

Buffering file kecil tercakup dalam bagian berikut dari topik ini:

Streaming

File diterima dari permintaan multipihak dan langsung diproses atau disimpan oleh aplikasi. Streaming tidak meningkatkan performa secara signifikan. Streaming mengurangi tuntutan memori atau ruang disk saat mengunggah file.

Streaming file besar tercakup dalam bagian Unggah file besar dengan streaming .

Mengunggah file kecil dengan pengikatan model buffer ke penyimpanan fisik

Untuk mengunggah file kecil, gunakan formulir multibagian atau buat permintaan POST menggunakan JavaScript.

Contoh berikut menunjukkan penggunaan Razor formulir Pages untuk mengunggah satu file (Pages/BufferedSingleFileUploadPhysical.cshtml di aplikasi sampel):

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

Contoh berikut dianalogikan dengan contoh sebelumnya kecuali bahwa:

  • JavaScript (Fetch API) digunakan untuk mengirimkan data formulir.
  • Tidak ada validasi.
<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>

Untuk melakukan formulir POST di JavaScript untuk klien yang tidak mendukung Fetch API, gunakan salah satu pendekatan berikut:

  • Gunakan Fetch Polyfill (misalnya, window.fetch polyfill (github/fetch)).

  • Gunakan XMLHttpRequest. Contohnya:

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

Untuk mendukung unggahan file, formulir HTML harus menentukan jenis pengodean (enctype) dari multipart/form-data.

files Agar elemen input mendukung pengunggahan beberapa file, berikan multiple atribut pada <input> elemen :

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

File individual yang diunggah ke server dapat diakses melalui Pengikatan Model menggunakan IFormFile. Aplikasi sampel menunjukkan beberapa unggahan file yang di-buffer untuk skenario penyimpanan database dan fisik.

Peringatan

Jangan gunakan FileName properti IFormFile selain untuk tampilan dan pengelogan. Saat menampilkan atau mencatat, HTML mengodekan nama file. Penyerang cyber dapat memberikan nama file berbahaya, termasuk jalur lengkap atau jalur relatif. Aplikasi harus:

  • Hapus jalur dari nama file yang disediakan pengguna.
  • Simpan nama file yang dikodekan HTML dan dihapus jalur untuk UI atau pengelogan.
  • Buat nama file acak baru untuk penyimpanan.

Kode berikut menghapus jalur dari nama file:

string untrustedFileName = Path.GetFileName(pathName);

Contoh yang diberikan sejauh ini tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Saat mengunggah file menggunakan pengikatan model dan IFormFile, metode tindakan dapat menerima:

Catatan

Pengikatan cocok dengan file formulir berdasarkan nama. Misalnya, nilai HTML name di <input type="file" name="formFile"> harus cocok dengan parameter C#/properti terikat (FormFile). Untuk informasi selengkapnya, lihat bagian Cocokkan nilai atribut nama dengan nama parameter metode POST.

Lihat contoh berikut:

  • Mengulangi satu atau beberapa file yang diunggah.
  • Menggunakan Path.GetTempFileName untuk mengembalikan jalur lengkap untuk file, termasuk nama file.
  • Menyimpan file ke sistem file lokal menggunakan nama file yang dihasilkan oleh aplikasi.
  • Mengembalikan jumlah total dan ukuran file yang diunggah.
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);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Gunakan Path.GetRandomFileName untuk menghasilkan nama file tanpa jalur. Dalam contoh berikut, jalur diperoleh dari konfigurasi:

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);
        }
    }
}

Jalur yang diteruskan ke FileStream harus menyertakan nama file. Jika nama file tidak disediakan, maka UnauthorizedAccessException akan dilemparkan pada runtime.

File yang diunggah menggunakan IFormFile teknik di-buffer dalam memori atau pada disk di server sebelum diproses. Di dalam metode tindakan, IFormFile konten dapat diakses sebagai Stream. Selain sistem file lokal, file dapat disimpan ke berbagi jaringan atau ke layanan penyimpanan file, seperti penyimpanan Azure Blob.

Untuk contoh lain yang mengulang beberapa file untuk diunggah dan menggunakan nama file yang aman, lihat Pages/BufferedMultipleFileUploadPhysical.cshtml.cs di aplikasi sampel.

Peringatan

Path.GetTempFileName melemparkan IOException jika lebih dari 65.535 file dibuat tanpa menghapus file sementara sebelumnya. Batas 65.535 file adalah batas per server. Untuk informasi selengkapnya tentang batas ini pada OS Windows, lihat komentar dalam topik berikut:

Mengunggah file kecil dengan pengikatan model buffer ke database

Untuk menyimpan data file biner dalam database menggunakan Entity Framework, tentukan Byte properti array pada entitas:

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

Tentukan properti model halaman untuk kelas yang menyertakan IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Catatan

IFormFile dapat digunakan langsung sebagai parameter metode tindakan atau sebagai properti model terikat. Contoh sebelumnya menggunakan properti model terikat.

FileUpload digunakan dalam Razor formulir Halaman:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Ketika formulir di-POS ke server, salin IFormFile ke aliran dan simpan sebagai array byte dalam database. Dalam contoh berikut, _dbContext menyimpan konteks database aplikasi:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 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();
}

Contoh sebelumnya mirip dengan skenario yang ditunjukkan dalam aplikasi sampel:

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

Peringatan

Berhati-hatilah saat menyimpan data biner dalam database relasional, karena dapat berdampak buruk pada performa.

Jangan mengandalkan FileName IFormFile atau mempercayai properti tanpa validasi. Properti FileName hanya boleh digunakan untuk tujuan tampilan dan hanya setelah pengodean HTML.

Contoh yang diberikan tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Mengunggah file besar dengan streaming

Contoh 3.1 menunjukkan cara menggunakan JavaScript untuk mengalirkan file ke tindakan pengontrol. Token antiforgery file dihasilkan menggunakan atribut filter kustom dan diteruskan ke header HTTP klien alih-alih di isi permintaan. Karena metode tindakan memproses data yang diunggah secara langsung, pengikatan model formulir dinonaktifkan oleh filter kustom lain. Dalam tindakan, konten formulir dibaca menggunakan MultipartReader, yang membaca setiap individu MultipartSection, memproses file atau menyimpan konten yang sesuai. Setelah bagian multibagian dibaca, tindakan melakukan pengikatan modelnya sendiri.

Respons halaman awal memuat formulir dan menyimpan token antiforgery dalam cookie (melalui GenerateAntiforgeryTokenCookieAttribute atribut ). Atribut ini menggunakan dukungan antiforgery bawaan ASP.NET Core untuk mengatur cookie dengan token permintaan:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute digunakan untuk menonaktifkan pengikatan model:

[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)
    {
    }
}

Di aplikasi sampel, GenerateAntiforgeryTokenCookieAttribute dan DisableFormValueModelBindingAttribute diterapkan sebagai filter ke model aplikasi halaman dari /StreamedSingleFileUploadDb dan /StreamedSingleFileUploadPhysical dalam Startup.ConfigureServices menggunakan Razor konvensi Pages:

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());
            });
});

Karena pengikatan model tidak membaca formulir, parameter yang terikat dari formulir tidak mengikat (kueri, rute, dan header terus berfungsi). Metode tindakan berfungsi langsung dengan Request properti . digunakan MultipartReader untuk membaca setiap bagian. Data kunci/nilai disimpan dalam KeyValueAccumulator. Setelah bagian multibagian dibaca, konten KeyValueAccumulator digunakan untuk mengikat data formulir ke jenis model.

Metode lengkap StreamingController.UploadDatabase untuk streaming ke database dengan EF Core:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

Metode lengkap StreamingController.UploadPhysical untuk streaming ke lokasi fisik:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

Di aplikasi sampel, pemeriksaan validasi ditangani oleh FileHelpers.ProcessStreamedFile.

Validasi

Kelas aplikasi FileHelpers sampel menunjukkan beberapa pemeriksaan untuk unggahan file yang di-buffer IFormFile dan dialirkan. Untuk memproses unggahan IFormFile file yang di-buffer di aplikasi sampel, lihat ProcessFormFile metode dalam Utilities/FileHelpers.cs file. Untuk memproses file yang dialirkan, lihat ProcessStreamedFile metode dalam file yang sama.

Peringatan

Metode pemrosesan validasi yang ditunjukkan dalam aplikasi sampel tidak memindai konten file yang diunggah. Dalam sebagian besar skenario produksi, API pemindai virus/malware digunakan pada file sebelum membuat file tersedia untuk pengguna atau sistem lain.

Meskipun sampel topik menyediakan contoh teknik validasi yang berfungsi, jangan terapkan FileHelpers kelas dalam aplikasi produksi kecuali Anda:

  • Memahami sepenuhnya implementasinya.
  • Ubah implementasi yang sesuai untuk lingkungan dan spesifikasi aplikasi.

Jangan pernah menerapkan kode keamanan tanpa sembarangan dalam aplikasi tanpa memenuhi persyaratan ini.

Validasi konten

Gunakan API pemindaian virus/malware pihak ketiga pada konten yang diunggah.

Memindai file menuntut sumber daya server dalam skenario volume tinggi. Jika performa pemrosesan permintaan berkurang karena pemindaian file, pertimbangkan untuk membongkar pekerjaan pemindaian ke layanan latar belakang, mungkin layanan yang berjalan di server yang berbeda dari server aplikasi. Biasanya, file yang diunggah disimpan di area yang dikarantina sampai pemindai virus latar belakang memeriksanya. Ketika file lolos, file dipindahkan ke lokasi penyimpanan file normal. Langkah-langkah ini biasanya dilakukan bersama dengan catatan database yang menunjukkan status pemindaian file. Dengan menggunakan pendekatan seperti itu, aplikasi dan server aplikasi tetap berfokus pada menanggapi permintaan.

Validasi ekstensi file

Ekstensi file yang diunggah harus diperiksa terhadap daftar ekstensi yang diizinkan. Contohnya:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Validasi tanda tangan file

Tanda tangan file ditentukan oleh beberapa byte pertama di awal file. Byte ini dapat digunakan untuk menunjukkan apakah ekstensi cocok dengan konten file. Aplikasi sampel memeriksa tanda tangan file untuk beberapa jenis file umum. Dalam contoh berikut, tanda tangan file untuk gambar JPEG diperiksa terhadap 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));
}

Untuk mendapatkan tanda tangan file tambahan, gunakan database tanda tangan file (hasil penelusuran Google) dan spesifikasi file resmi. Konsultasi spesifikasi file resmi dapat memastikan bahwa tanda tangan yang dipilih valid.

Keamanan nama file

Jangan pernah menggunakan nama file yang disediakan klien untuk menyimpan file ke penyimpanan fisik. Buat nama file yang aman untuk file menggunakan Path.GetRandomFileName atau Path.GetTempFileName untuk membuat jalur lengkap (termasuk nama file) untuk penyimpanan sementara.

Razor html otomatis mengodekan nilai properti untuk ditampilkan. Kode berikut aman digunakan:

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

RazorDi luar , selalu HtmlEncode nama file konten dari permintaan pengguna.

Banyak implementasi harus menyertakan pemeriksaan bahwa file ada; jika tidak, file ditimpa oleh file dengan nama yang sama. Berikan logika tambahan untuk memenuhi spesifikasi aplikasi Anda.

Validasi bagi ukuran

Batasi ukuran file yang diunggah.

Dalam aplikasi sampel, ukuran file dibatasi hingga 2 MB (ditunjukkan dalam byte). Batas disediakan melalui Konfigurasi dari appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit disuntikkan ke dalam PageModel kelas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Ketika ukuran file melebihi batas, file ditolak:

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

Cocokkan nilai atribut nama dengan nama parameter metode POST

Dalam bentuk non-formulirRazor yang memformat data POST atau menggunakan JavaScript FormData secara langsung, nama yang ditentukan dalam elemen formulir atau FormData harus cocok dengan nama parameter dalam tindakan pengontrol.

Dalam contoh berikut:

  • Saat menggunakan <input> elemen, name atribut diatur ke nilai battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Saat menggunakan FormData di JavaScript, nama diatur ke nilai battlePlans:

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

Gunakan nama yang cocok untuk parameter metode C# (battlePlans):

  • Razor Untuk metode handler halaman Pages bernama Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Untuk metode tindakan pengontrol POST MVC:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

Konfigurasi server dan aplikasi

Batas panjang isi multipihak

MultipartBodyLengthLimit menetapkan batas untuk panjang setiap isi multipihak. Bagian formulir yang melebihi batas ini melempar InvalidDataException saat diurai. Defaultnya adalah 134.217.728 (128 MB). Sesuaikan batas menggunakan MultipartBodyLengthLimit pengaturan di Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

RequestFormLimitsAttribute digunakan untuk mengatur MultipartBodyLengthLimit untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model.Filters.Add(
                new RequestFormLimitsAttribute()
                {
                    // Set the limit to 256 MB
                    MultipartBodyLengthLimit = 268435456
                });
});

Razor Di aplikasi Pages atau aplikasi MVC, terapkan filter ke model halaman atau metode tindakan:

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel ukuran isi permintaan maksimum

Untuk aplikasi yang dihosting oleh Kestrel, ukuran isi permintaan maksimum default adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas menggunakan opsi server MaxRequestBodySizeKestrel :

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                // Handle requests up to 50 MB
                options.Limits.MaxRequestBodySize = 52428800;
            })
            .UseStartup<Startup>();
        });

RequestSizeLimitAttribute digunakan untuk mengatur MaxRequestBodySize untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});

Razor Di aplikasi halaman atau aplikasi MVC, terapkan filter ke kelas handler halaman atau metode tindakan:

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

RequestSizeLimitAttribute juga dapat diterapkan menggunakan direktif@attributeRazor:

@attribute [RequestSizeLimitAttribute(52428800)]

Batasan lainnya Kestrel

Batas lain Kestrel dapat berlaku untuk aplikasi yang dihosting oleh Kestrel:

IIS

Batas permintaan default (maxAllowedContentLength) adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas dalam web.config file. Dalam contoh berikut, batas diatur ke 50 MB (52.428.800 byte):

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

Pengaturan maxAllowedContentLength hanya berlaku untuk IIS. Untuk informasi selengkapnya, lihat Batas <requestLimits>Permintaan .

Pecahkan masalah

Di bawah ini adalah beberapa masalah umum yang dihadapi saat bekerja dengan mengunggah file dan kemungkinan solusinya.

Kesalahan Tidak Ditemukan saat disebarkan ke server IIS

Kesalahan berikut menunjukkan bahwa file yang diunggah melebihi panjang konten server yang dikonfigurasi:

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

Untuk informasi selengkapnya, lihat bagian IIS .

Kegagalan koneksi

Kesalahan koneksi dan koneksi server reset mungkin menunjukkan bahwa file yang diunggah melebihi Kestrelukuran isi permintaan maksimum. Untuk informasi selengkapnya, lihat bagian ukuran isi Kestrel permintaan maksimum. Kestrel batas koneksi klien mungkin juga memerlukan penyesuaian.

Pengecualian Referensi Null dengan IFormFile

Jika pengontrol menerima file yang diunggah menggunakan IFormFile tetapi nilainya adalah null, konfirmasikan bahwa formulir HTML menentukan enctype nilai multipart/form-data. Jika atribut ini tidak diatur pada <form> elemen , unggahan file tidak terjadi dan argumen terikat IFormFile apa pun adalah null. Konfirmasikan juga bahwa penamaan unggahan dalam data formulir cocok dengan penamaan aplikasi.

Aliran terlalu panjang

Contoh dalam topik ini diandalkan MemoryStream untuk menyimpan konten file yang diunggah. Batas ukuran adalah MemoryStream int.MaxValue. Jika skenario pengunggahan file aplikasi memerlukan penahanan konten file yang lebih besar dari 50 MB, gunakan pendekatan alternatif yang tidak bergantung pada satu untuk MemoryStream menyimpan konten file yang diunggah.

ASP.NET Core mendukung pengunggahan satu atau beberapa file menggunakan pengikatan model buffer untuk file yang lebih kecil dan streaming yang tidak dibuffer untuk file yang lebih besar.

Melihat atau mengunduh kode sampel (cara mengunduh)

Pertimbangan keamanan

Berhati-hatilah saat memberi pengguna kemampuan untuk mengunggah file ke server. Cyberattackers dapat mencoba untuk:

  • Jalankan penolakan serangan layanan .
  • Unggah virus atau malware.
  • Membahayakan jaringan dan server dengan cara lain.

Langkah-langkah keamanan yang mengurangi kemungkinan serangan yang berhasil adalah:

  • Unggah file ke area unggahan file khusus, sebaiknya ke drive non-sistem. Lokasi khusus memudahkan untuk memberlakukan pembatasan keamanan pada file yang diunggah. Nonaktifkan izin eksekusi pada location.† unggahan file
  • Jangan menyimpan file yang diunggah di pohon direktori yang sama dengan app.†
  • Gunakan nama file aman yang ditentukan oleh aplikasi. Jangan gunakan nama file yang disediakan oleh pengguna atau nama file yang tidak tepercaya dari file yang diunggah.† HTML mengodekan nama file yang tidak tepercaya saat menampilkannya. Misalnya, mencatat nama file atau menampilkan di UI (Razor output pengodean HTML secara otomatis).
  • Izinkan hanya ekstensi file yang disetujui untuk spesifikasi desain aplikasi.†
  • Verifikasi bahwa pemeriksaan sisi klien dilakukan pada pemeriksaan sisi klien server.† mudah disinggung.
  • Periksa ukuran file yang diunggah. Atur batas ukuran maksimum untuk mencegah uploads.† besar
  • Ketika file tidak boleh ditimpa oleh file yang diunggah dengan nama yang sama, periksa nama file terhadap database atau penyimpanan fisik sebelum mengunggah file.
  • Jalankan pemindai virus/malware pada konten yang diunggah sebelum file disimpan.

† Aplikasi sampel menunjukkan pendekatan yang memenuhi kriteria.

Peringatan

Mengunggah kode berbahaya ke sistem sering kali merupakan langkah pertama untuk mengeksekusi kode yang dapat:

  • Sepenuhnya mendapatkan kendali atas sistem.
  • Membebani sistem secara berlebih dengan hasil yang dilumpuhkan sistem.
  • Membahayakan data pengguna atau sistem.
  • Terapkan grafiti ke UI publik.

Untuk informasi tentang mengurangi kerentanan saat menerima file dari pengguna, lihat sumber daya berikut:

Untuk informasi selengkapnya tentang menerapkan langkah-langkah keamanan, termasuk contoh dari aplikasi sampel, lihat bagian Validasi .

Skenario penyimpanan

Opsi penyimpanan umum untuk file meliputi:

  • Database

    • Untuk unggahan file kecil, database sering kali lebih cepat daripada opsi penyimpanan fisik (sistem file atau berbagi jaringan).
    • Database sering lebih nyaman daripada opsi penyimpanan fisik karena pengambilan rekaman database untuk data pengguna dapat secara bersamaan menyediakan konten file (misalnya, gambar avatar).
    • Database berpotensi lebih murah daripada menggunakan layanan penyimpanan data.
  • Penyimpanan fisik (sistem file atau berbagi jaringan)

    • Untuk unggahan file besar:
      • Batas database dapat membatasi ukuran unggahan.
      • Penyimpanan fisik sering kali kurang ekonomis daripada penyimpanan dalam database.
    • Penyimpanan fisik berpotensi lebih murah daripada menggunakan layanan penyimpanan data.
    • Proses aplikasi harus memiliki izin baca dan tulis ke lokasi penyimpanan. Jangan pernah memberikan izin eksekusi.
  • Layanan penyimpanan data (misalnya, Azure Blob Storage)

    • Layanan biasanya menawarkan peningkatan skalabilitas dan ketahanan atas solusi lokal yang biasanya tunduk pada satu titik kegagalan.
    • Layanan berpotensi menurunkan biaya dalam skenario infrastruktur penyimpanan besar.

    Untuk informasi selengkapnya, lihat Mulai Cepat: Menggunakan .NET untuk membuat blob di penyimpanan objek.

Skenario unggahan file

Dua pendekatan umum untuk mengunggah file adalah buffering dan streaming.

Buffering

Seluruh file dibaca ke dalam IFormFile, yang merupakan representasi C# dari file yang digunakan untuk memproses atau menyimpan file.

Sumber daya (disk, memori) yang digunakan oleh unggahan file bergantung pada jumlah dan ukuran unggahan file bersamaan. Jika aplikasi mencoba buffer terlalu banyak unggahan, situs mengalami crash saat kehabisan memori atau ruang disk. Jika ukuran atau frekuensi unggahan file melelahkan sumber daya aplikasi, gunakan streaming.

Catatan

Setiap file buffer tunggal yang melebihi 64 KB dipindahkan dari memori ke file sementara pada disk.

Buffering file kecil tercakup dalam bagian berikut dari topik ini:

Streaming

File diterima dari permintaan multipihak dan langsung diproses atau disimpan oleh aplikasi. Streaming tidak meningkatkan performa secara signifikan. Streaming mengurangi tuntutan memori atau ruang disk saat mengunggah file.

Streaming file besar tercakup dalam bagian Unggah file besar dengan streaming .

Mengunggah file kecil dengan pengikatan model buffer ke penyimpanan fisik

Untuk mengunggah file kecil, gunakan formulir multibagian atau buat permintaan POST menggunakan JavaScript.

Contoh berikut menunjukkan penggunaan Razor formulir Pages untuk mengunggah satu file (Pages/BufferedSingleFileUploadPhysical.cshtml di aplikasi sampel):

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

Contoh berikut dianalogikan dengan contoh sebelumnya kecuali bahwa:

  • JavaScript (Fetch API) digunakan untuk mengirimkan data formulir.
  • Tidak ada validasi.
<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>

Untuk melakukan formulir POST di JavaScript untuk klien yang tidak mendukung Fetch API, gunakan salah satu pendekatan berikut:

  • Gunakan Fetch Polyfill (misalnya, window.fetch polyfill (github/fetch)).

  • Gunakan XMLHttpRequest. Contohnya:

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

Untuk mendukung unggahan file, formulir HTML harus menentukan jenis pengodean (enctype) dari multipart/form-data.

files Agar elemen input mendukung pengunggahan beberapa file, berikan multiple atribut pada <input> elemen :

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

File individual yang diunggah ke server dapat diakses melalui Pengikatan Model menggunakan IFormFile. Aplikasi sampel menunjukkan beberapa unggahan file yang di-buffer untuk skenario penyimpanan database dan fisik.

Peringatan

Jangan gunakan FileName properti IFormFile selain untuk tampilan dan pengelogan. Saat menampilkan atau mencatat, HTML mengodekan nama file. Penyerang cyber dapat memberikan nama file berbahaya, termasuk jalur lengkap atau jalur relatif. Aplikasi harus:

  • Hapus jalur dari nama file yang disediakan pengguna.
  • Simpan nama file yang dikodekan HTML dan dihapus jalur untuk UI atau pengelogan.
  • Buat nama file acak baru untuk penyimpanan.

Kode berikut menghapus jalur dari nama file:

string untrustedFileName = Path.GetFileName(pathName);

Contoh yang diberikan sejauh ini tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Saat mengunggah file menggunakan pengikatan model dan IFormFile, metode tindakan dapat menerima:

Catatan

Pengikatan cocok dengan file formulir berdasarkan nama. Misalnya, nilai HTML name di <input type="file" name="formFile"> harus cocok dengan parameter C#/properti terikat (FormFile). Untuk informasi selengkapnya, lihat bagian Cocokkan nilai atribut nama dengan nama parameter metode POST.

Lihat contoh berikut:

  • Mengulangi satu atau beberapa file yang diunggah.
  • Menggunakan Path.GetTempFileName untuk mengembalikan jalur lengkap untuk file, termasuk nama file.
  • Menyimpan file ke sistem file lokal menggunakan nama file yang dihasilkan oleh aplikasi.
  • Mengembalikan jumlah total dan ukuran file yang diunggah.
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);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Gunakan Path.GetRandomFileName untuk menghasilkan nama file tanpa jalur. Dalam contoh berikut, jalur diperoleh dari konfigurasi:

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);
        }
    }
}

Jalur yang diteruskan ke FileStream harus menyertakan nama file. Jika nama file tidak disediakan, maka UnauthorizedAccessException akan dilemparkan pada runtime.

File yang diunggah menggunakan IFormFile teknik di-buffer dalam memori atau pada disk di server sebelum diproses. Di dalam metode tindakan, IFormFile konten dapat diakses sebagai Stream. Selain sistem file lokal, file dapat disimpan ke berbagi jaringan atau ke layanan penyimpanan file, seperti penyimpanan Azure Blob.

Untuk contoh lain yang mengulang beberapa file untuk diunggah dan menggunakan nama file yang aman, lihat Pages/BufferedMultipleFileUploadPhysical.cshtml.cs di aplikasi sampel.

Peringatan

Path.GetTempFileName melemparkan IOException jika lebih dari 65.535 file dibuat tanpa menghapus file sementara sebelumnya. Batas 65.535 file adalah batas per server. Untuk informasi selengkapnya tentang batas ini pada OS Windows, lihat komentar dalam topik berikut:

Mengunggah file kecil dengan pengikatan model buffer ke database

Untuk menyimpan data file biner dalam database menggunakan Entity Framework, tentukan Byte properti array pada entitas:

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

Tentukan properti model halaman untuk kelas yang menyertakan IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Catatan

IFormFile dapat digunakan langsung sebagai parameter metode tindakan atau sebagai properti model terikat. Contoh sebelumnya menggunakan properti model terikat.

FileUpload digunakan dalam Razor formulir Halaman:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Ketika formulir di-POS ke server, salin IFormFile ke aliran dan simpan sebagai array byte dalam database. Dalam contoh berikut, _dbContext menyimpan konteks database aplikasi:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 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();
}

Contoh sebelumnya mirip dengan skenario yang ditunjukkan dalam aplikasi sampel:

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

Peringatan

Berhati-hatilah saat menyimpan data biner dalam database relasional, karena dapat berdampak buruk pada performa.

Jangan mengandalkan FileName IFormFile atau mempercayai properti tanpa validasi. Properti FileName hanya boleh digunakan untuk tujuan tampilan dan hanya setelah pengodean HTML.

Contoh yang diberikan tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Mengunggah file besar dengan streaming

Contoh berikut menunjukkan cara menggunakan JavaScript untuk mengalirkan file ke tindakan pengontrol. Token antiforgery file dihasilkan menggunakan atribut filter kustom dan diteruskan ke header HTTP klien alih-alih di isi permintaan. Karena metode tindakan memproses data yang diunggah secara langsung, pengikatan model formulir dinonaktifkan oleh filter kustom lain. Dalam tindakan, konten formulir dibaca menggunakan MultipartReader, yang membaca setiap individu MultipartSection, memproses file atau menyimpan konten yang sesuai. Setelah bagian multibagian dibaca, tindakan melakukan pengikatan modelnya sendiri.

Respons halaman awal memuat formulir dan menyimpan token antiforgery dalam cookie (melalui GenerateAntiforgeryTokenCookieAttribute atribut ). Atribut ini menggunakan dukungan antiforgery bawaan ASP.NET Core untuk mengatur cookie dengan token permintaan:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute digunakan untuk menonaktifkan pengikatan model:

[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)
    {
    }
}

Di aplikasi sampel, GenerateAntiforgeryTokenCookieAttribute dan DisableFormValueModelBindingAttribute diterapkan sebagai filter ke model aplikasi halaman dari /StreamedSingleFileUploadDb dan /StreamedSingleFileUploadPhysical dalam Startup.ConfigureServices menggunakan Razor konvensi Pages:

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());
            });
});

Karena pengikatan model tidak membaca formulir, parameter yang terikat dari formulir tidak mengikat (kueri, rute, dan header terus berfungsi). Metode tindakan berfungsi langsung dengan Request properti . digunakan MultipartReader untuk membaca setiap bagian. Data kunci/nilai disimpan dalam KeyValueAccumulator. Setelah bagian multibagian dibaca, konten KeyValueAccumulator digunakan untuk mengikat data formulir ke jenis model.

Metode lengkap StreamingController.UploadDatabase untuk streaming ke database dengan EF Core:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

Metode lengkap StreamingController.UploadPhysical untuk streaming ke lokasi fisik:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

Di aplikasi sampel, pemeriksaan validasi ditangani oleh FileHelpers.ProcessStreamedFile.

Validasi

Kelas aplikasi FileHelpers sampel menunjukkan beberapa pemeriksaan untuk unggahan file yang di-buffer IFormFile dan dialirkan. Untuk memproses unggahan IFormFile file yang di-buffer di aplikasi sampel, lihat ProcessFormFile metode dalam Utilities/FileHelpers.cs file. Untuk memproses file yang dialirkan, lihat ProcessStreamedFile metode dalam file yang sama.

Peringatan

Metode pemrosesan validasi yang ditunjukkan dalam aplikasi sampel tidak memindai konten file yang diunggah. Dalam sebagian besar skenario produksi, API pemindai virus/malware digunakan pada file sebelum membuat file tersedia untuk pengguna atau sistem lain.

Meskipun sampel topik menyediakan contoh teknik validasi yang berfungsi, jangan terapkan FileHelpers kelas dalam aplikasi produksi kecuali Anda:

  • Memahami sepenuhnya implementasinya.
  • Ubah implementasi yang sesuai untuk lingkungan dan spesifikasi aplikasi.

Jangan pernah menerapkan kode keamanan tanpa sembarangan dalam aplikasi tanpa memenuhi persyaratan ini.

Validasi konten

Gunakan API pemindaian virus/malware pihak ketiga pada konten yang diunggah.

Memindai file menuntut sumber daya server dalam skenario volume tinggi. Jika performa pemrosesan permintaan berkurang karena pemindaian file, pertimbangkan untuk membongkar pekerjaan pemindaian ke layanan latar belakang, mungkin layanan yang berjalan di server yang berbeda dari server aplikasi. Biasanya, file yang diunggah disimpan di area yang dikarantina sampai pemindai virus latar belakang memeriksanya. Ketika file lolos, file dipindahkan ke lokasi penyimpanan file normal. Langkah-langkah ini biasanya dilakukan bersama dengan catatan database yang menunjukkan status pemindaian file. Dengan menggunakan pendekatan seperti itu, aplikasi dan server aplikasi tetap berfokus pada menanggapi permintaan.

Validasi ekstensi file

Ekstensi file yang diunggah harus diperiksa terhadap daftar ekstensi yang diizinkan. Contohnya:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Validasi tanda tangan file

Tanda tangan file ditentukan oleh beberapa byte pertama di awal file. Byte ini dapat digunakan untuk menunjukkan apakah ekstensi cocok dengan konten file. Aplikasi sampel memeriksa tanda tangan file untuk beberapa jenis file umum. Dalam contoh berikut, tanda tangan file untuk gambar JPEG diperiksa terhadap 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));
}

Untuk mendapatkan tanda tangan file tambahan, gunakan database tanda tangan file (hasil penelusuran Google) dan spesifikasi file resmi. Konsultasi spesifikasi file resmi dapat memastikan bahwa tanda tangan yang dipilih valid.

Keamanan nama file

Jangan pernah menggunakan nama file yang disediakan klien untuk menyimpan file ke penyimpanan fisik. Buat nama file yang aman untuk file menggunakan Path.GetRandomFileName atau Path.GetTempFileName untuk membuat jalur lengkap (termasuk nama file) untuk penyimpanan sementara.

Razor html otomatis mengodekan nilai properti untuk ditampilkan. Kode berikut aman digunakan:

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

RazorDi luar , selalu HtmlEncode nama file konten dari permintaan pengguna.

Banyak implementasi harus menyertakan pemeriksaan bahwa file ada; jika tidak, file ditimpa oleh file dengan nama yang sama. Berikan logika tambahan untuk memenuhi spesifikasi aplikasi Anda.

Validasi bagi ukuran

Batasi ukuran file yang diunggah.

Dalam aplikasi sampel, ukuran file dibatasi hingga 2 MB (ditunjukkan dalam byte). Batas disediakan melalui Konfigurasi dari appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit disuntikkan ke dalam PageModel kelas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Ketika ukuran file melebihi batas, file ditolak:

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

Cocokkan nilai atribut nama dengan nama parameter metode POST

Dalam bentuk non-formulirRazor yang memformat data POST atau menggunakan JavaScript FormData secara langsung, nama yang ditentukan dalam elemen formulir atau FormData harus cocok dengan nama parameter dalam tindakan pengontrol.

Dalam contoh berikut:

  • Saat menggunakan <input> elemen, name atribut diatur ke nilai battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Saat menggunakan FormData di JavaScript, nama diatur ke nilai battlePlans:

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

Gunakan nama yang cocok untuk parameter metode C# (battlePlans):

  • Razor Untuk metode handler halaman Pages bernama Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Untuk metode tindakan pengontrol POST MVC:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

Konfigurasi server dan aplikasi

Batas panjang isi multipihak

MultipartBodyLengthLimit menetapkan batas untuk panjang setiap isi multipihak. Bagian formulir yang melebihi batas ini melempar InvalidDataException saat diurai. Defaultnya adalah 134.217.728 (128 MB). Sesuaikan batas menggunakan MultipartBodyLengthLimit pengaturan di Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

RequestFormLimitsAttribute digunakan untuk mengatur MultipartBodyLengthLimit untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model.Filters.Add(
                new RequestFormLimitsAttribute()
                {
                    // Set the limit to 256 MB
                    MultipartBodyLengthLimit = 268435456
                });
});

Razor Di aplikasi Pages atau aplikasi MVC, terapkan filter ke model halaman atau metode tindakan:

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel ukuran isi permintaan maksimum

Untuk aplikasi yang dihosting oleh Kestrel, ukuran isi permintaan maksimum default adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas menggunakan opsi server MaxRequestBodySizeKestrel :

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                // Handle requests up to 50 MB
                options.Limits.MaxRequestBodySize = 52428800;
            })
            .UseStartup<Startup>();
        });

RequestSizeLimitAttribute digunakan untuk mengatur MaxRequestBodySize untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});

Razor Di aplikasi halaman atau aplikasi MVC, terapkan filter ke kelas handler halaman atau metode tindakan:

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

RequestSizeLimitAttribute juga dapat diterapkan menggunakan direktif@attributeRazor:

@attribute [RequestSizeLimitAttribute(52428800)]

Batasan lainnya Kestrel

Batas lain Kestrel dapat berlaku untuk aplikasi yang dihosting oleh Kestrel:

IIS

Batas permintaan default (maxAllowedContentLength) adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas dalam web.config file. Dalam contoh berikut, batas diatur ke 50 MB (52.428.800 byte):

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

Pengaturan maxAllowedContentLength hanya berlaku untuk IIS. Untuk informasi selengkapnya, lihat Batas <requestLimits>Permintaan .

Tingkatkan ukuran isi permintaan maksimum untuk permintaan HTTP dengan mengatur IISServerOptions.MaxRequestBodySize di Startup.ConfigureServices. Dalam contoh berikut, batas diatur ke 50 MB (52.428.800 byte):

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

Untuk informasi selengkapnya, lihat Host ASP.NET Core di Windows dengan IIS.

Pecahkan masalah

Di bawah ini adalah beberapa masalah umum yang dihadapi saat bekerja dengan mengunggah file dan kemungkinan solusinya.

Kesalahan Tidak Ditemukan saat disebarkan ke server IIS

Kesalahan berikut menunjukkan bahwa file yang diunggah melebihi panjang konten server yang dikonfigurasi:

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

Untuk informasi selengkapnya, lihat bagian IIS .

Kegagalan koneksi

Kesalahan koneksi dan koneksi server reset mungkin menunjukkan bahwa file yang diunggah melebihi Kestrelukuran isi permintaan maksimum. Untuk informasi selengkapnya, lihat bagian ukuran isi Kestrel permintaan maksimum. Kestrel batas koneksi klien mungkin juga memerlukan penyesuaian.

Pengecualian Referensi Null dengan IFormFile

Jika pengontrol menerima file yang diunggah menggunakan IFormFile tetapi nilainya adalah null, konfirmasikan bahwa formulir HTML menentukan enctype nilai multipart/form-data. Jika atribut ini tidak diatur pada <form> elemen , unggahan file tidak terjadi dan argumen terikat IFormFile apa pun adalah null. Konfirmasikan juga bahwa penamaan unggahan dalam data formulir cocok dengan penamaan aplikasi.

Aliran terlalu panjang

Contoh dalam topik ini diandalkan MemoryStream untuk menyimpan konten file yang diunggah. Batas ukuran adalah MemoryStream int.MaxValue. Jika skenario pengunggahan file aplikasi memerlukan penahanan konten file yang lebih besar dari 50 MB, gunakan pendekatan alternatif yang tidak bergantung pada satu untuk MemoryStream menyimpan konten file yang diunggah.

ASP.NET Core mendukung pengunggahan satu atau beberapa file menggunakan pengikatan model buffer untuk file yang lebih kecil dan streaming yang tidak dibuffer untuk file yang lebih besar.

Melihat atau mengunduh kode sampel (cara mengunduh)

Pertimbangan keamanan

Berhati-hatilah saat memberi pengguna kemampuan untuk mengunggah file ke server. Cyberattackers dapat mencoba untuk:

  • Jalankan penolakan serangan layanan .
  • Unggah virus atau malware.
  • Membahayakan jaringan dan server dengan cara lain.

Langkah-langkah keamanan yang mengurangi kemungkinan serangan yang berhasil adalah:

  • Unggah file ke area unggahan file khusus, sebaiknya ke drive non-sistem. Lokasi khusus memudahkan untuk memberlakukan pembatasan keamanan pada file yang diunggah. Nonaktifkan izin eksekusi pada location.† unggahan file
  • Jangan menyimpan file yang diunggah di pohon direktori yang sama dengan app.†
  • Gunakan nama file aman yang ditentukan oleh aplikasi. Jangan gunakan nama file yang disediakan oleh pengguna atau nama file yang tidak tepercaya dari file yang diunggah.† HTML mengodekan nama file yang tidak tepercaya saat menampilkannya. Misalnya, mencatat nama file atau menampilkan di UI (Razor output pengodean HTML secara otomatis).
  • Izinkan hanya ekstensi file yang disetujui untuk spesifikasi desain aplikasi.†
  • Verifikasi bahwa pemeriksaan sisi klien dilakukan pada pemeriksaan sisi klien server.† mudah disinggung.
  • Periksa ukuran file yang diunggah. Atur batas ukuran maksimum untuk mencegah uploads.† besar
  • Ketika file tidak boleh ditimpa oleh file yang diunggah dengan nama yang sama, periksa nama file terhadap database atau penyimpanan fisik sebelum mengunggah file.
  • Jalankan pemindai virus/malware pada konten yang diunggah sebelum file disimpan.

† Aplikasi sampel menunjukkan pendekatan yang memenuhi kriteria.

Peringatan

Mengunggah kode berbahaya ke sistem sering kali merupakan langkah pertama untuk mengeksekusi kode yang dapat:

  • Sepenuhnya mendapatkan kendali atas sistem.
  • Membebani sistem secara berlebih dengan hasil yang dilumpuhkan sistem.
  • Membahayakan data pengguna atau sistem.
  • Terapkan grafiti ke UI publik.

Untuk informasi tentang mengurangi kerentanan saat menerima file dari pengguna, lihat sumber daya berikut:

Untuk informasi selengkapnya tentang menerapkan langkah-langkah keamanan, termasuk contoh dari aplikasi sampel, lihat bagian Validasi .

Skenario penyimpanan

Opsi penyimpanan umum untuk file meliputi:

  • Database

    • Untuk unggahan file kecil, database sering kali lebih cepat daripada opsi penyimpanan fisik (sistem file atau berbagi jaringan).
    • Database sering lebih nyaman daripada opsi penyimpanan fisik karena pengambilan rekaman database untuk data pengguna dapat secara bersamaan menyediakan konten file (misalnya, gambar avatar).
    • Database berpotensi lebih murah daripada menggunakan layanan penyimpanan data.
  • Penyimpanan fisik (sistem file atau berbagi jaringan)

    • Untuk unggahan file besar:
      • Batas database dapat membatasi ukuran unggahan.
      • Penyimpanan fisik sering kali kurang ekonomis daripada penyimpanan dalam database.
    • Penyimpanan fisik berpotensi lebih murah daripada menggunakan layanan penyimpanan data.
    • Proses aplikasi harus memiliki izin baca dan tulis ke lokasi penyimpanan. Jangan pernah memberikan izin eksekusi.
  • Layanan penyimpanan data (misalnya, Azure Blob Storage)

    • Layanan biasanya menawarkan peningkatan skalabilitas dan ketahanan atas solusi lokal yang biasanya tunduk pada satu titik kegagalan.
    • Layanan berpotensi menurunkan biaya dalam skenario infrastruktur penyimpanan besar.

    Untuk informasi selengkapnya, lihat Mulai Cepat: Menggunakan .NET untuk membuat blob di penyimpanan objek. Topik ini menunjukkan UploadFromFileAsync, tetapi UploadFromStreamAsync dapat digunakan untuk menyimpan FileStream penyimpanan blob saat bekerja dengan Stream.

Skenario unggahan file

Dua pendekatan umum untuk mengunggah file adalah buffering dan streaming.

Buffering

Seluruh file dibaca ke dalam IFormFile, yang merupakan representasi C# dari file yang digunakan untuk memproses atau menyimpan file.

Sumber daya (disk, memori) yang digunakan oleh unggahan file bergantung pada jumlah dan ukuran unggahan file bersamaan. Jika aplikasi mencoba buffer terlalu banyak unggahan, situs mengalami crash saat kehabisan memori atau ruang disk. Jika ukuran atau frekuensi unggahan file melelahkan sumber daya aplikasi, gunakan streaming.

Catatan

Setiap file buffer tunggal yang melebihi 64 KB dipindahkan dari memori ke file sementara pada disk.

Buffering file kecil tercakup dalam bagian berikut dari topik ini:

Streaming

File diterima dari permintaan multipihak dan langsung diproses atau disimpan oleh aplikasi. Streaming tidak meningkatkan performa secara signifikan. Streaming mengurangi tuntutan memori atau ruang disk saat mengunggah file.

Streaming file besar tercakup dalam bagian Unggah file besar dengan streaming .

Mengunggah file kecil dengan pengikatan model buffer ke penyimpanan fisik

Untuk mengunggah file kecil, gunakan formulir multibagian atau buat permintaan POST menggunakan JavaScript.

Contoh berikut menunjukkan penggunaan Razor formulir Pages untuk mengunggah satu file (Pages/BufferedSingleFileUploadPhysical.cshtml di aplikasi sampel):

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

Contoh berikut dianalogikan dengan contoh sebelumnya kecuali bahwa:

  • JavaScript (Fetch API) digunakan untuk mengirimkan data formulir.
  • Tidak ada validasi.
<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>

Untuk melakukan formulir POST di JavaScript untuk klien yang tidak mendukung Fetch API, gunakan salah satu pendekatan berikut:

  • Gunakan Fetch Polyfill (misalnya, window.fetch polyfill (github/fetch)).

  • Gunakan XMLHttpRequest. Contohnya:

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

Untuk mendukung unggahan file, formulir HTML harus menentukan jenis pengodean (enctype) dari multipart/form-data.

files Agar elemen input mendukung pengunggahan beberapa file, berikan multiple atribut pada <input> elemen :

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

File individual yang diunggah ke server dapat diakses melalui Pengikatan Model menggunakan IFormFile. Aplikasi sampel menunjukkan beberapa unggahan file yang di-buffer untuk skenario penyimpanan database dan fisik.

Peringatan

Jangan gunakan FileName properti IFormFile selain untuk tampilan dan pengelogan. Saat menampilkan atau mencatat, HTML mengodekan nama file. Penyerang cyber dapat memberikan nama file berbahaya, termasuk jalur lengkap atau jalur relatif. Aplikasi harus:

  • Hapus jalur dari nama file yang disediakan pengguna.
  • Simpan nama file yang dikodekan HTML dan dihapus jalur untuk UI atau pengelogan.
  • Buat nama file acak baru untuk penyimpanan.

Kode berikut menghapus jalur dari nama file:

string untrustedFileName = Path.GetFileName(pathName);

Contoh yang diberikan sejauh ini tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Saat mengunggah file menggunakan pengikatan model dan IFormFile, metode tindakan dapat menerima:

Catatan

Pengikatan cocok dengan file formulir berdasarkan nama. Misalnya, nilai HTML name di <input type="file" name="formFile"> harus cocok dengan parameter C#/properti terikat (FormFile). Untuk informasi selengkapnya, lihat bagian Cocokkan nilai atribut nama dengan nama parameter metode POST.

Lihat contoh berikut:

  • Mengulangi satu atau beberapa file yang diunggah.
  • Menggunakan Path.GetTempFileName untuk mengembalikan jalur lengkap untuk file, termasuk nama file.
  • Menyimpan file ke sistem file lokal menggunakan nama file yang dihasilkan oleh aplikasi.
  • Mengembalikan jumlah total dan ukuran file yang diunggah.
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);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Gunakan Path.GetRandomFileName untuk menghasilkan nama file tanpa jalur. Dalam contoh berikut, jalur diperoleh dari konfigurasi:

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);
        }
    }
}

Jalur yang diteruskan ke FileStream harus menyertakan nama file. Jika nama file tidak disediakan, maka UnauthorizedAccessException akan dilemparkan pada runtime.

File yang diunggah menggunakan IFormFile teknik di-buffer dalam memori atau pada disk di server sebelum diproses. Di dalam metode tindakan, IFormFile konten dapat diakses sebagai Stream. Selain sistem file lokal, file dapat disimpan ke berbagi jaringan atau ke layanan penyimpanan file, seperti penyimpanan Azure Blob.

Untuk contoh lain yang mengulang beberapa file untuk diunggah dan menggunakan nama file yang aman, lihat Pages/BufferedMultipleFileUploadPhysical.cshtml.cs di aplikasi sampel.

Peringatan

Path.GetTempFileName melemparkan IOException jika lebih dari 65.535 file dibuat tanpa menghapus file sementara sebelumnya. Batas 65.535 file adalah batas per server. Untuk informasi selengkapnya tentang batas ini pada OS Windows, lihat komentar dalam topik berikut:

Mengunggah file kecil dengan pengikatan model buffer ke database

Untuk menyimpan data file biner dalam database menggunakan Entity Framework, tentukan Byte properti array pada entitas:

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

Tentukan properti model halaman untuk kelas yang menyertakan IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Catatan

IFormFile dapat digunakan langsung sebagai parameter metode tindakan atau sebagai properti model terikat. Contoh sebelumnya menggunakan properti model terikat.

FileUpload digunakan dalam Razor formulir Halaman:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Ketika formulir di-POS ke server, salin IFormFile ke aliran dan simpan sebagai array byte dalam database. Dalam contoh berikut, _dbContext menyimpan konteks database aplikasi:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 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();
}

Contoh sebelumnya mirip dengan skenario yang ditunjukkan dalam aplikasi sampel:

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

Peringatan

Berhati-hatilah saat menyimpan data biner dalam database relasional, karena dapat berdampak buruk pada performa.

Jangan mengandalkan FileName IFormFile atau mempercayai properti tanpa validasi. Properti FileName hanya boleh digunakan untuk tujuan tampilan dan hanya setelah pengodean HTML.

Contoh yang diberikan tidak memperhitungkan pertimbangan keamanan. Informasi tambahan disediakan oleh bagian berikut dan aplikasi sampel:

Mengunggah file besar dengan streaming

Contoh berikut menunjukkan cara menggunakan JavaScript untuk mengalirkan file ke tindakan pengontrol. Token antiforgery file dihasilkan menggunakan atribut filter kustom dan diteruskan ke header HTTP klien alih-alih di isi permintaan. Karena metode tindakan memproses data yang diunggah secara langsung, pengikatan model formulir dinonaktifkan oleh filter kustom lain. Dalam tindakan, konten formulir dibaca menggunakan MultipartReader, yang membaca setiap individu MultipartSection, memproses file atau menyimpan konten yang sesuai. Setelah bagian multibagian dibaca, tindakan melakukan pengikatan modelnya sendiri.

Respons halaman awal memuat formulir dan menyimpan token antiforgery dalam cookie (melalui GenerateAntiforgeryTokenCookieAttribute atribut ). Atribut ini menggunakan dukungan antiforgery bawaan ASP.NET Core untuk mengatur cookie dengan token permintaan:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute digunakan untuk menonaktifkan pengikatan model:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

Di aplikasi sampel, GenerateAntiforgeryTokenCookieAttribute dan DisableFormValueModelBindingAttribute diterapkan sebagai filter ke model aplikasi halaman dari /StreamedSingleFileUploadDb dan /StreamedSingleFileUploadPhysical dalam Startup.ConfigureServices menggunakan Razor konvensi Pages:

services.AddMvc()
    .AddRazorPagesOptions(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());
                    });
        })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Karena pengikatan model tidak membaca formulir, parameter yang terikat dari formulir tidak mengikat (kueri, rute, dan header terus berfungsi). Metode tindakan berfungsi langsung dengan Request properti . digunakan MultipartReader untuk membaca setiap bagian. Data kunci/nilai disimpan dalam KeyValueAccumulator. Setelah bagian multibagian dibaca, konten KeyValueAccumulator digunakan untuk mengikat data formulir ke jenis model.

Metode lengkap StreamingController.UploadDatabase untuk streaming ke database dengan EF Core:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

Metode lengkap StreamingController.UploadPhysical untuk streaming ke lokasi fisik:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

Di aplikasi sampel, pemeriksaan validasi ditangani oleh FileHelpers.ProcessStreamedFile.

Validasi

Kelas aplikasi FileHelpers sampel menunjukkan beberapa pemeriksaan untuk unggahan file yang di-buffer IFormFile dan dialirkan. Untuk memproses unggahan IFormFile file yang di-buffer di aplikasi sampel, lihat ProcessFormFile metode dalam Utilities/FileHelpers.cs file. Untuk memproses file yang dialirkan, lihat ProcessStreamedFile metode dalam file yang sama.

Peringatan

Metode pemrosesan validasi yang ditunjukkan dalam aplikasi sampel tidak memindai konten file yang diunggah. Dalam sebagian besar skenario produksi, API pemindai virus/malware digunakan pada file sebelum membuat file tersedia untuk pengguna atau sistem lain.

Meskipun sampel topik menyediakan contoh teknik validasi yang berfungsi, jangan terapkan FileHelpers kelas dalam aplikasi produksi kecuali Anda:

  • Memahami sepenuhnya implementasinya.
  • Ubah implementasi yang sesuai untuk lingkungan dan spesifikasi aplikasi.

Jangan pernah menerapkan kode keamanan tanpa sembarangan dalam aplikasi tanpa memenuhi persyaratan ini.

Validasi konten

Gunakan API pemindaian virus/malware pihak ketiga pada konten yang diunggah.

Memindai file menuntut sumber daya server dalam skenario volume tinggi. Jika performa pemrosesan permintaan berkurang karena pemindaian file, pertimbangkan untuk membongkar pekerjaan pemindaian ke layanan latar belakang, mungkin layanan yang berjalan di server yang berbeda dari server aplikasi. Biasanya, file yang diunggah disimpan di area yang dikarantina sampai pemindai virus latar belakang memeriksanya. Ketika file lolos, file dipindahkan ke lokasi penyimpanan file normal. Langkah-langkah ini biasanya dilakukan bersama dengan catatan database yang menunjukkan status pemindaian file. Dengan menggunakan pendekatan seperti itu, aplikasi dan server aplikasi tetap berfokus pada menanggapi permintaan.

Validasi ekstensi file

Ekstensi file yang diunggah harus diperiksa terhadap daftar ekstensi yang diizinkan. Contohnya:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Validasi tanda tangan file

Tanda tangan file ditentukan oleh beberapa byte pertama di awal file. Byte ini dapat digunakan untuk menunjukkan apakah ekstensi cocok dengan konten file. Aplikasi sampel memeriksa tanda tangan file untuk beberapa jenis file umum. Dalam contoh berikut, tanda tangan file untuk gambar JPEG diperiksa terhadap 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));
}

Untuk mendapatkan tanda tangan file tambahan, gunakan database tanda tangan file (hasil penelusuran Google) dan spesifikasi file resmi. Konsultasi spesifikasi file resmi dapat memastikan bahwa tanda tangan yang dipilih valid.

Keamanan nama file

Jangan pernah menggunakan nama file yang disediakan klien untuk menyimpan file ke penyimpanan fisik. Buat nama file yang aman untuk file menggunakan Path.GetRandomFileName atau Path.GetTempFileName untuk membuat jalur lengkap (termasuk nama file) untuk penyimpanan sementara.

Razor html otomatis mengodekan nilai properti untuk ditampilkan. Kode berikut aman digunakan:

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

RazorDi luar , selalu HtmlEncode nama file konten dari permintaan pengguna.

Banyak implementasi harus menyertakan pemeriksaan bahwa file ada; jika tidak, file ditimpa oleh file dengan nama yang sama. Berikan logika tambahan untuk memenuhi spesifikasi aplikasi Anda.

Validasi bagi ukuran

Batasi ukuran file yang diunggah.

Dalam aplikasi sampel, ukuran file dibatasi hingga 2 MB (ditunjukkan dalam byte). Batas disediakan melalui Konfigurasi dari appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit disuntikkan ke dalam PageModel kelas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Ketika ukuran file melebihi batas, file ditolak:

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

Cocokkan nilai atribut nama dengan nama parameter metode POST

Dalam bentuk non-formulirRazor yang memformat data POST atau menggunakan JavaScript FormData secara langsung, nama yang ditentukan dalam elemen formulir atau FormData harus cocok dengan nama parameter dalam tindakan pengontrol.

Dalam contoh berikut:

  • Saat menggunakan <input> elemen, name atribut diatur ke nilai battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Saat menggunakan FormData di JavaScript, nama diatur ke nilai battlePlans:

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

Gunakan nama yang cocok untuk parameter metode C# (battlePlans):

  • Razor Untuk metode handler halaman Pages bernama Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Untuk metode tindakan pengontrol POST MVC:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

Konfigurasi server dan aplikasi

Batas panjang isi multipihak

MultipartBodyLengthLimit menetapkan batas untuk panjang setiap isi multipihak. Bagian formulir yang melebihi batas ini melempar InvalidDataException saat diurai. Defaultnya adalah 134.217.728 (128 MB). Sesuaikan batas menggunakan MultipartBodyLengthLimit pengaturan di Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

RequestFormLimitsAttribute digunakan untuk mengatur MultipartBodyLengthLimit untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model.Filters.Add(
                    new RequestFormLimitsAttribute()
                    {
                        // Set the limit to 256 MB
                        MultipartBodyLengthLimit = 268435456
                    });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Razor Di aplikasi Pages atau aplikasi MVC, terapkan filter ke model halaman atau metode tindakan:

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel ukuran isi permintaan maksimum

Untuk aplikasi yang dihosting oleh Kestrel, ukuran isi permintaan maksimum default adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas menggunakan opsi server MaxRequestBodySizeKestrel :

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel((context, options) =>
        {
            // Handle requests up to 50 MB
            options.Limits.MaxRequestBodySize = 52428800;
        });

RequestSizeLimitAttribute digunakan untuk mengatur MaxRequestBodySize untuk satu halaman atau tindakan.

Razor Di aplikasi Pages, terapkan filter dengan konvensi di Startup.ConfigureServices:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model =>
                {
                    // Handle requests up to 50 MB
                    model.Filters.Add(
                        new RequestSizeLimitAttribute(52428800));
                });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Razor Di aplikasi halaman atau aplikasi MVC, terapkan filter ke kelas handler halaman atau metode tindakan:

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Batasan lainnya Kestrel

Batas lain Kestrel dapat berlaku untuk aplikasi yang dihosting oleh Kestrel:

IIS

Batas permintaan default (maxAllowedContentLength) adalah 30.000.000 byte, yaitu sekitar 28,6 MB. Sesuaikan batas dalam web.config file. Dalam contoh berikut, batas diatur ke 50 MB (52.428.800 byte):

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

Pengaturan maxAllowedContentLength hanya berlaku untuk IIS. Untuk informasi selengkapnya, lihat Batas <requestLimits>Permintaan .

Tingkatkan ukuran isi permintaan maksimum untuk permintaan HTTP dengan mengatur IISServerOptions.MaxRequestBodySize di Startup.ConfigureServices. Dalam contoh berikut, batas diatur ke 50 MB (52.428.800 byte):

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

Untuk informasi selengkapnya, lihat Host ASP.NET Core di Windows dengan IIS.

Pecahkan masalah

Di bawah ini adalah beberapa masalah umum yang dihadapi saat bekerja dengan mengunggah file dan kemungkinan solusinya.

Kesalahan Tidak Ditemukan saat disebarkan ke server IIS

Kesalahan berikut menunjukkan bahwa file yang diunggah melebihi panjang konten server yang dikonfigurasi:

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

Untuk informasi selengkapnya, lihat bagian IIS .

Kegagalan koneksi

Kesalahan koneksi dan koneksi server reset mungkin menunjukkan bahwa file yang diunggah melebihi Kestrelukuran isi permintaan maksimum. Untuk informasi selengkapnya, lihat bagian ukuran isi Kestrel permintaan maksimum. Kestrel batas koneksi klien mungkin juga memerlukan penyesuaian.

Pengecualian Referensi Null dengan IFormFile

Jika pengontrol menerima file yang diunggah menggunakan IFormFile tetapi nilainya adalah null, konfirmasikan bahwa formulir HTML menentukan enctype nilai multipart/form-data. Jika atribut ini tidak diatur pada <form> elemen , unggahan file tidak terjadi dan argumen terikat IFormFile apa pun adalah null. Konfirmasikan juga bahwa penamaan unggahan dalam data formulir cocok dengan penamaan aplikasi.

Aliran terlalu panjang

Contoh dalam topik ini diandalkan MemoryStream untuk menyimpan konten file yang diunggah. Batas ukuran adalah MemoryStream int.MaxValue. Jika skenario pengunggahan file aplikasi memerlukan penahanan konten file yang lebih besar dari 50 MB, gunakan pendekatan alternatif yang tidak bergantung pada satu untuk MemoryStream menyimpan konten file yang diunggah.

Sumber Daya Tambahan: