ヘッダー
C# Sample Programs
 

File Uploads

01/07/2024

This article deals with ASP.NET Core Razor Pages.

 

All sample programs in this article are based on the Visual Studio Project template "ASP.NET Core Web App" and you must add the Razor pages in th Pages folder.

 

 

Upload a file by a File input and save the file to server side folder

In this Example, You can upload a file with the specified extension ( .txt,.csv,.tsv,.gif,.png,.jpg,.jpeg,.zip,.xlsx,.pdf ). And then file will saved in folder C:\temp\upload at the server. The saved file name will not match the uploaded file name for security reasons. If you run this example in your computer, the "server" means your computer.

 

FileUploadSingle.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Net;

namespace WorkStandard.Pages.controls;

public class FileUploadSingleModel : PageModel
{
    [BindProperty]
    [DisplayName("File input test")]
    [Required(ErrorMessage = "No file selected.")]
    public IFormFile? uploadFile { get; set; }

    public string result { get; set; } = "";

    public void OnPostUpload()
    {

        //▼Check errors that can be automatically detected. (Maybe no file selected)
        if (!ModelState.IsValid)
        {
            return;
        }

        //▼Ensure the file is not empty.
        if (uploadFile!.Length == 0)
        {
            ModelState.AddModelError(nameof(uploadFile), "Empty file not allowed.");
            return;
        }

        //▼Ensure the file is not too large.
        // The meaning of "large" depends on the case.For ease of test, I will limit the file size to 20MB.
        // Accepting no size limit file upload will derives DoS attack.
        // In case of GB size file, it's better to use streaming instead of this sample.
        else if (uploadFile.Length > 20 * 1024 * 1024)
        {
            ModelState.AddModelError(nameof(uploadFile), "Over 20MB file not allowed.");
            return;
        }


        //▼Sanitize the file name
        //The file names may be crafted variously for attacks.
        //So you must not trust the uploaded raw file name. You must generate safe file name to save it.
        //Example: Test💀.png → ee4zdnerbe2-Test.png

        //Uploaded raw file name.(danger)
        string rawFileName = Path.GetFileName(uploadFile.FileName);

        //Remove characters except non-alphanumeric characters and the last dot (if any).
        int dotCount = rawFileName.Count(c => c == '.');
        string asciiName = new string(rawFileName.Where(c =>
            (c == '.' && --dotCount == 0) || (char.IsAsciiLetterOrDigit(c))
        ).ToArray());

        //Prepend random characters and - at top.
        string safeFileName = Path.GetRandomFileName().Replace(".", "") + "-" + asciiName;

        //In this sample, the file will be saved to C:\temp\upload. you must create this folder before.
        //Do not save upload files in the program folder or wwwroot. (You will be serious attacked.)
        string path = Path.Combine(@"C:\temp\upload", safeFileName);

        //▼Check file path length

        //In case of  Windows10 1607 later, the path length can be more than 260 characters.
        const int MAX_PATH = 260;
        if (path.Length > MAX_PATH)
        {
            ModelState.AddModelError(nameof(uploadFile), "Too long file name.");
            return;
        }

        //▼Check file already exists
        //This will last chance to protect against overwriting system files, etc.
        if (System.IO.File.Exists(path))
        {
            ModelState.AddModelError(nameof(uploadFile), "Not acceptable file name. Please change the file name.");
            return;
        }

        //▼File extension validations
        //In this sample, allowed exntensions are .txt .csv .tsv .gif .png .jpg .jpeg .zip .xlsx .pdf .
        //At here, verify the extension is much to the contents of binary file.
        //Extremely high risc extensions for example .exe will be rejected by web browser security features.

        //If you want to accept all files, comment these validation codes.
        //However, It increases the risk of saving malicious contents , such as being attacked just by opening it.

        string EXT = Path.GetExtension(uploadFile.FileName).ToUpperInvariant();

        //Load contents of the upload file to memory.
        using MemoryStream memoryStream = new();
        uploadFile.CopyTo(memoryStream);

        memoryStream.Position = 0;
        byte[] header;
        using (var reader = new BinaryReader(memoryStream))
        {
            //Read first 10 bytes, for now.
            header = reader.ReadBytes((int)Math.Min(10, memoryStream.Length));
        }

        //Signatures to verify contents
        List<byte[]> signatures = new();

        if (EXT is ".TXT" or ".CSV" or ".TSV")
        {
            //non-binary files are OK. (it doesn't means safe completely.)
        }
        else if (EXT is ".GIF")
            signatures = new List<byte[]> {
                new byte[] { 0x47, 0x49, 0x46, 0x38 }
            };
        else if (EXT is ".PNG")
            signatures = new List<byte[]> {
                new byte[]{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }
            };
        else if (EXT is ".JPG" or ".JPEG")
            signatures = new List<byte[]> {
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
            };
        else if (EXT is ".ZIP" or ".XLSX")
            signatures = new List<byte[]> {
                new byte[] { 0x50, 0x4B, 0x03, 0x04 },
                new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
                new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
                new byte[] { 0x50, 0x4B, 0x05, 0x06 },
                new byte[] { 0x50, 0x4B, 0x07, 0x08 },
                new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
            };
        else if (EXT is ".PDF")
            signatures = new List<byte[]> {
                new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D }
            };
        else
        {
            ModelState.AddModelError(nameof(uploadFile), "The extension not allowed.");
            return;
        }

        //If binary files, compare actual file header with signatures. 
        //At here, signatures.Count > 0 means the file is binary.
        if (signatures.Count > 0)
        {
            bool extOK = false;
            foreach (var signature in signatures)
            {
                if (header.Take(signature.Length).SequenceEqual(signature))
                {
                    extOK = true;
                }
            }

            if (!extOK)
            {
                ModelState.AddModelError(nameof(uploadFile), "Invalid file content.");
                return;
            }
        }

        //▼Save the uoload file
        byte[] content = memoryStream.ToArray();

        using (var fileStream = System.IO.File.Create(path))
        {
            fileStream.Write(content);
        }

        //Please also check the contents of the file using antivirus software.
        //Like email attachments, It may execute malicious functions if opened the save file carelessly.

        //▼Output the result on the screen
        string safeFileNameForDisplay = WebUtility.HtmlEncode(uploadFile.FileName);
        result = $"{safeFileNameForDisplay} uploaded.";

    } //OnPostUpload
}

 

FileUploadSingle.cshtml

@page
@model WorkStandard.Pages.controls.FileUploadSingleModel

<form method="post" enctype="multipart/form-data">
    <div>
        <label asp-for="uploadFile" class="form-label"></label>
        <input asp-for="uploadFile" class="form-control" 
            accept=".txt,.csv,.tsv,.gif,.png,.jpg,.jpeg,.zip,.xlsx,.pdf" />
    </div>
    <div>
        <span asp-validation-for="uploadFile" class="text-danger"></span>
        <p class="text-success">@Model.result</p>
    </div>
    <div>
        <input type="submit" value="Upload" asp-page-handler="Upload" />
    </div>
</form>

@section Scripts {
    @{
        //Enable client side validation。
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

The CSS classes...

  • form-control Make the input a Bootstrap look.→ Forms
  • text-danger The string become red to indicate danger.→ Theme colors
  • text-success The string become green to indicate success.→ Theme colors
  • btn btn-primary Give the button a Bootstrap look to indicate that it's a key feature.→ Buttons

Note: In this sample, The uploadable extensions are limited to .txt,.csv,.tsv,.gif,.png,.jpg,.jpeg,.zip,.xlsx,.pdf. You can remove this limitation by commenting out about 70 lines following "▼File Extension Validations" in cs and removing accept attribute in chhtml. But note that accepting limitless extentions increase the risc to receive malicious files.

Note: This sample is just only check signatures to validate file extensions, so it's possible to upload extension disguised file with same signatures file types. For example, If you change the .xlsm file extension to .xlsx, the upload will be successful. And .txt files are not checked, so if you disguise the extension as .txt, the upload will be successful.

Note: To allow other extensions, you must know the valid signature(Normaly it's some byte at top of file) by checking  the specification of the file.   The publicly available signature database will help you. see https://www.google.com/search?q=file+signatures+databases&hl=en .

 

 

Upload a file [simple version] (insecure)

This sample is just only for educational purposes. you must NOT use for production because of mainly high security risc.

FileUploadSingleInsecure.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel;

namespace WorkStandard.Pages.controls
{
    public class FileUploadSingleInsecureModel : PageModel
    {
        [BindProperty]
        [DisplayName("File input test")]
        public IFormFile? uploadFile { get; set; }

        public void OnPostUpload()
        {
            System.Diagnostics.Debug.WriteLine($"The uploaded file:{uploadFile!.FileName}");
            string path = Path.Combine(@"C:\temp\upload", Path.GetFileName(uploadFile!.FileName));

            using FileStream stream = new(path, FileMode.Create);
            uploadFile.CopyTo(stream);
        }

    }
}

Where Debug.WriteLine outputs to

 

FileUploadSingle.cshtml

@page
@model WorkStandard.Pages.controls.FileUploadSingleInsecureModel

<form method="post" enctype="multipart/form-data">
    <div>
        <label asp-for="uploadFile" class="form-label"></label>
        <input asp-for="uploadFile" class="form-control" />
    </div>
    <div>
        <input type="submit" value="Upload" asp-page-handler="Upload" />
    </div>
</form>

The CSS classes...

  • form-control Make the input a Bootstrap look.→ Forms

Note: This sample is just only for educational purposes. For practical usage please check Note:Considerations of the file upload

 

 

Upload multiple files [simple version] (insecure)

This sample is just only for educational purposes. you must NOT use for production because of mainly high security risc.

FileUploadMultipleInsecure.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel;

namespace WorkStandard.Pages.controls
{
    public class FileUploadMultipleInsecureModel : PageModel
    {
        [BindProperty]
        [DisplayName("File input test multiple version")]
        public List<IFormFile>? uploadFiles { get; set; }

        public void OnPostUpload()
        {
            foreach(IFormFile uploadFile in uploadFiles!)
            {
                System.Diagnostics.Debug.WriteLine($"The uploaded file:{uploadFile!.FileName}");
                string path = Path.Combine(@"C:\temp\upload", Path.GetFileName(uploadFile!.FileName));

                using FileStream stream = new(path, FileMode.Create);
                uploadFile.CopyTo(stream);
            }
        }

    }
}

Where Debug.WriteLine outputs to

 

FileUploadMultipleInsecure.cshtml

@page
@model WorkStandard.Pages.controls.FileUploadMultipleInsecureModel

<form method="post" enctype="multipart/form-data">
    <div>
        <label asp-for="uploadFiles" class="form-label"></label>
        <input asp-for="uploadFiles" class="form-control" multiple />
    </div>
    <div>
        <input type="submit" value="Upload" asp-page-handler="Upload" />
    </div>
</form>

The CSS classes...

  • form-control Make the input a Bootstrap look.→ Forms

Note: This sample is just only for educational purposes. For practical usage please check Note:Considerations of the file upload

 

 

Note:Considerations of the file upload

  • The file upload feature is vulnerable to powerful attacks and is dangerous. It will occur overwriting important system files by directory traversal attacks or sending some kind of viruses or suspending service by uploading tons of huge files. And if you mistake processing file name or contents, it will derive more serious attacks for example sql injection or os command injection and so on.
  • The Following site provide us further useful precautions including security and you can see some samples to upload files. I saw the site to writing this article. https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads
  • This is the direct link to the sample codes. AspNetCore.Docs/aspnetcore/mvc/models/file-uploads/samples at main · dotnet/AspNetCore.Docs (github.com) ※There are many samples in the site. It's strange, but pressing . (dot) on the keyboard changes the screen. On that screen, you can download the sample by right-clicking on the tree on the left.(But it doesn't work now, I don't know why...)
  • In view point of security, It's better to save uploaded content in the database instead of save as files.

 


日本語版