ASP.NET Core에서 파일 업로드Upload files in ASP.NET Core

작성자, Steve SmithRutger 스톰By Steve Smith and Rutger Storm

ASP.NET Core는 소용량 파일의 경우에는 버퍼링된 모델 바인딩을 사용하여 하나 이상의 파일을 업로드하고, 대용량 파일의 경우에는 버퍼링되지 않은 스트리밍을 지원합니다.ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

예제 코드 살펴보기 및 다운로드 (다운로드 방법)View or download sample code (how to download)

보안 고려 사항Security considerations

사용자에게 서버에 파일을 업로드하는 기능을 제공할 때는 주의해야 합니다.Use caution when providing users with the ability to upload files to a server. 공격자는 다음을 시도할 수 있습니다.Attackers may attempt to:

  • 서비스 거부 공격을 실행.Execute denial of service attacks.
  • 바이러스 또는 맬웨어를 업로드.Upload viruses or malware.
  • 다른 방법으로 네트워크 및 서버를 손상.Compromise networks and servers in other ways.

공격이 성공할 가능성을 줄이는 보안 단계는 다음과 같습니다.Security steps that reduce the likelihood of a successful attack are:

  • 전용 파일 업로드 영역(바람직하게는 시스템 드라이브가 아닌 위치)에 파일을 업로드합니다.Upload files to a dedicated file upload area, preferably to a non-system drive. 전용 위치를 사용하면 업로드된 파일에 대한 보안 제한을 더 쉽게 적용할 수 있습니다.A dedicated location makes it easier to impose security restrictions on uploaded files. 파일 업로드 위치에 대한 실행 권한을 사용하지 않도록 설정합니다.†Disable execute permissions on the file upload location.†
  • 업로드된 파일을 앱과 동일한 디렉터리 트리에 보관하지 마세요.†Do not persist uploaded files in the same directory tree as the app.†
  • 앱에 의해 결정된 안전한 파일 이름을 사용합니다.Use a safe file name determined by the app. 사용자가 제공한 파일 이름 또는 업로드 된 파일의 신뢰할 수 없는 파일 이름을 사용 하지 마세요. † HTML을 표시 하는 경우 신뢰할 수 없는 파일 이름을 인코딩합니다.Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. 예를 들어 파일 이름을 기록 하거나 UI에 표시 하는 경우 ( Razor 자동으로 HTML 인코딩 출력)For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
  • 앱의 디자인 사양으로 승인된 파일 확장명만 허용합니다.†Allow only approved file extensions for the app's design specification.†
  • 서버에서 클라이언트 쪽 검사가 수행 되는지 확인 합니다. † 클라이언트 쪽 검사는 쉽게 피할 수 있습니다.Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
  • 업로드된 파일의 크기를 확인합니다.Check the size of an uploaded file. 대규모로 업로드되지 않도록 최대 크기 제한을 설정합니다.†Set a maximum size limit to prevent large uploads.†
  • 업로드된 파일이 같은 이름의 파일을 덮어쓰면 안 되는 경우 파일을 업로드하기 전에 데이터베이스 또는 실제 스토리지에서 파일 이름을 확인합니다.When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
  • 파일이 저장되기 전에 업로드된 콘텐츠에 대해 바이러스/맬웨어 스캐너를 실행합니다.Run a virus/malware scanner on uploaded content before the file is stored.

†샘플 앱은 조건을 충족하는 방법을 보여 줍니다.†The sample app demonstrates an approach that meets the criteria.

경고

시스템에 악성 코드를 업로드하는 행위는 흔히 다음을 수행할 수 있는 코드를 실행하기 위한 첫 단계가 됩니다.Uploading malicious code to a system is frequently the first step to executing code that can:

  • 시스템을 완전히 제어합니다.Completely gain control of a system.
  • 시스템 작동이 중단되는 결과로 시스템을 오버로드합니다.Overload a system with the result that the system crashes.
  • 사용자 또는 시스템 데이터를 손상시킵니다.Compromise user or system data.
  • 공용 UI에 그래피티를 적용합니다.Apply graffiti to a public UI.

사용자의 파일을 수락할 때 공격 노출 영역을 줄이는 방법에 대한 자세한 내용은 다음 리소스를 참조하세요.For information on reducing the attack surface area when accepting files from users, see the following resources:

샘플 앱의 예제를 포함하여 보안 조치를 구현하는 방법에 대한 자세한 내용은 유효성 검사 섹션을 참조하세요.For more information on implementing security measures, including examples from the sample app, see the Validation section.

스토리지 시나리오Storage scenarios

파일에 대한 일반적인 스토리지 옵션은 다음과 같습니다.Common storage options for files include:

  • 데이터베이스Database

    • 소용량 파일 업로드의 경우 데이터베이스는 실제 스토리지(파일 시스템 또는 네트워크 공유) 옵션보다 빠른 경우가 많습니다.For small file uploads, a database is often faster than physical storage (file system or network share) options.
    • 사용자 데이터에 대한 데이터베이스 레코드를 검색할 때 파일 콘텐츠(예: 아바타 이미지)를 동시에 제공할 수 있으므로 데이터베이스는 실제 스토리지 옵션보다 편리합니다.A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
    • 데이터베이스는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.A database is potentially less expensive than using a data storage service.
  • 실제 스토리지(파일 시스템 또는 네트워크 공유)Physical storage (file system or network share)

    • 대용량 파일 업로드의 경우:For large file uploads:
      • 데이터베이스 한도 때문에 업로드 크기가 제한될 수 있습니다.Database limits may restrict the size of the upload.
      • 실제 스토리지는 데이터베이스 스토리지보다 경제적이지 않은 경우가 자주 있습니다.Physical storage is often less economical than storage in a database.
    • 실제 스토리지는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.Physical storage is potentially less expensive than using a data storage service.
    • 앱의 프로세스에는 스토리지 위치에 대한 읽기 및 쓰기 권한이 있어야 합니다.The app's process must have read and write permissions to the storage location. 실행 권한을 부여하지 마세요.Never grant execute permission.
  • 데이터 스토리지 서비스(예: Azure Blob Storage)Data storage service (for example, Azure Blob Storage)

    • 서비스는 일반적으로 단일 실패 지점에 노출되는 온-프레미스 솔루션에 비해 향상된 확장성 및 복원력을 제공합니다.Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
    • 서비스는 대용량 스토리지 인프라 시나리오에서 비용이 더 저렴할 수 있습니다.Services are potentially lower cost in large storage infrastructure scenarios.

    자세한 내용은 빠른 시작: .net을 사용 하 여 개체 저장소에 blob 만들기를 참조 하세요.For more information, see Quickstart: Use .NET to create a blob in object storage.

파일 업로드 시나리오File upload scenarios

파일 업로드를 위한 일반적인 방법 두 가지는 버퍼링 및 스트리밍입니다.Two general approaches for uploading files are buffering and streaming.

버퍼링Buffering

전체 파일을 파일 처리 또는 저장에 사용되는 파일의 C# 표현인 IFormFile로 읽어 들입니다.The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

파일 업로드에서 사용되는 리소스(디스크, 메모리)는 동시 파일 업로드 크기와 수에 따라 달라집니다.The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. 앱이 너무 많은 업로드를 버퍼링하려 할 경우 메모리 또는 디스크 공간이 부족하면 사이트의 작동이 중단됩니다.If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. 파일 업로드의 크기 또는 빈도로 인해 앱 리소스가 소진되는 경우 스트리밍을 사용합니다.If the size or frequency of file uploads is exhausting app resources, use streaming.

참고

버퍼링된 단일 파일이 64KB를 초과하는 경우 메모리에서 디스크의 임시 파일로 이동됩니다.Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

소용량 파일 버퍼링은 이 항목의 다음 섹션에서 설명합니다.Buffering small files is covered in the following sections of this topic:

스트리밍Streaming

파일은 다중 파트 요청에서 수신되며 앱에서 직접 처리하거나 저장합니다.The file is received from a multipart request and directly processed or saved by the app. 스트리밍은 성능을 크게 개선하지 않습니다.Streaming doesn't improve performance significantly. 스트리밍을 통해 파일을 업로드하면 메모리 또는 디스크 공간에 대한 요구가 줄어듭니다.Streaming reduces the demands for memory or disk space when uploading files.

대용량 파일 스트리밍은 스트리밍을 사용하여 대용량 파일 업로드 섹션에서 설명합니다.Streaming large files is covered in the Upload large files with streaming section.

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 실제 스토리지에 업로드Upload small files with buffered model binding to physical storage

소용량 파일을 업로드하려면 다중 파트 양식을 사용하거나 JavaScript를 사용하여 POST 요청을 생성합니다.To upload small files, use a multipart form or construct a POST request using JavaScript.

다음 예제에서는 Razor pages 폼을 사용 하 여 단일 파일 (샘플 응용 프로그램의 Pages/BufferedSingleFileUploadPhysical )을 업로드 하는 방법을 보여 줍니다.The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

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

다음 예제는 이전 예제와 비슷하지만 다음과 같은 차이가 있습니다.The following example is analogous to the prior example except that:

  • 양식의 데이터를 제출하는 데 JavaScript(Fetch API)가 사용됩니다.JavaScript's (Fetch API) is used to submit the form's data.
  • 유효성 검사를 수행하지 않습니다.There's no validation.
<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>

Fetch API를 지원하지 않는 클라이언트에 대해 JavaScript로 양식 POST를 수행하려면 다음 방법 중 하나를 사용합니다.To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:

  • Fetch Polyfill(예: window.fetch polyfill (github/fetch))을 사용합니다.Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).

  • XMLHttpRequest을 사용하세요.Use XMLHttpRequest. 예를 들면 다음과 같습니다.For example:

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

파일 업로드를 지원하려면 HTML 양식에서 multipart/form-data의 인코딩 유형(enctype)을 지정해야 합니다.In order to support file uploads, HTML forms must specify an encoding type (enctype) of multipart/form-data.

files 입력 요소가 다중 파일 업로드를 지원하려면 <input> 요소에 multiple 특성을 제공합니다.For a files input element to support uploading multiple files provide the multiple attribute on the <input> element:

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

서버에 업로드된 개별 파일은 IFormFile을 사용하여 모델 바인딩을 통해 액세스할 수 있습니다.The individual files uploaded to the server can be accessed through Model Binding using IFormFile. 샘플 앱은 데이터베이스 및 실제 스토리지 시나리오에 대한 다중 버퍼링된 파일 업로드를 보여 줍니다.The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

경고

표시 및 로깅 이외에는 IFormFileFileName 속성을 사용하지 마세요.Do not use the FileName property of IFormFile other than for display and logging. 표시하거나 로깅할 경우 파일 이름을 HTML로 인코딩합니다.When displaying or logging, HTML encode the file name. 공격자는 전체 경로나 상대 경로를 포함하여 악의적인 파일 이름을 제공할 수 있습니다.An attacker can provide a malicious filename, including full paths or relative paths. 애플리케이션에서 다음을 수행해야 합니다.Applications should:

  • 사용자가 제공한 파일 이름에서 경로를 제거합니다.Remove the path from the user-supplied filename.
  • UI 또는 로깅을 위해 HTML로 인코딩되고 경로가 제거된 파일 이름을 저장합니다.Save the HTML-encoded, path-removed filename for UI or logging.
  • 스토리지의 새 임의 파일 이름을 생성합니다.Generate a new random filename for storage.

다음 코드는 파일 이름에서 경로를 제거합니다.The following code removes the path from the file name:

string untrustedFileName = Path.GetFileName(pathName);

지금까지 제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided thus far don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

모델 바인딩 및 IFormFile을 사용하여 파일을 업로드하는 경우 작업 메서드에서 다음을 허용할 수 있습니다.When uploading files using model binding and IFormFile, the action method can accept:

참고

바인딩은 이름을 기준으로 양식 파일을 일치시킵니다.Binding matches form files by name. 예를 들어 <input type="file" name="formFile">의 HTML name 값은 바인딩된 C# 매개 변수/속성(FormFile)과 일치해야 합니다.For example, the HTML name value in <input type="file" name="formFile"> must match the C# parameter/property bound (FormFile). 자세한 내용은 이름 특성 값을 POST 메서드의 매개 변수 이름과 일치 섹션을 참조하세요.For more information, see the Match name attribute value to parameter name of POST method section.

다음 예제가 하는 일:The following example:

  • 하나 이상의 업로드된 파일을 반복합니다.Loops through one or more uploaded files.
  • Path.GetTempFileName을 사용하여 파일 이름을 포함하는 파일 전체 경로를 반환합니다.Uses Path.GetTempFileName to return a full path for a file, including the file name.
  • 앱에서 생성한 파일 이름을 사용하여 로컬 파일 시스템에 파일을 저장합니다.Saves the files to the local file system using a file name generated by the app.
  • 업로드된 파일의 총 수와 크기를 반환합니다.Returns the total number and size of files uploaded.
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 });
}

Path.GetRandomFileName을 사용하여 경로 없이 파일 이름을 생성합니다.Use Path.GetRandomFileName to generate a file name without a path. 다음 예제에서는 경로를 구성에서 가져옵니다.In the following example, the path is obtained from configuration:

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

에 전달된 경로는 FileStream 반드시 파일 이름을 포함해야 합니다.The path passed to the FileStream must include the file name. 파일 이름을 제공하지 않으면 런타임에 UnauthorizedAccessException이 발생합니다.If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.

IFormFile 기술을 사용하여 업로드된 파일은 처리 전에 서버의 메모리나 디스크에 버퍼링됩니다.Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. 작업 메서드 내부에서 IFormFile 내용을 Stream으로 액세스할 수 있습니다.Inside the action method, the IFormFile contents are accessible as a Stream. 로컬 파일 시스템 외에도 파일을 네트워크 공유 또는 파일 스토리지 서비스(예: Azure Blob Storage)에 저장할 수 있습니다.In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.

업로드를 위해 여러 파일에 대해 루프를 실행하고 안전한 파일 이름을 사용하는 또 하나의 예제는 샘플 앱에서 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 를 참조하세요.For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

경고

이전 임시 파일을 삭제하지 않고 65,535개를 초과하는 파일을 만들면 Path.GetTempFileNameIOException을 throw합니다.Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. 65535개 파일 제한은 서버당 제한입니다.The limit of 65,535 files is a per-server limit. Windows OS에서 이 제한에 대한 자세한 내용은 다음 항목의 설명을 참조하세요.For more information on this limit on Windows OS, see the remarks in the following topics:

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 데이터베이스에 업로드Upload small files with buffered model binding to a database

Entity Framework를 사용하여 데이터베이스에 이진 파일 데이터를 저장하려면 엔터티에서 Byte 배열 속성을 정의합니다.To store binary file data in a database using Entity Framework, define a Byte array property on the entity:

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

IFormFile을 포함하는 클래스에 대한 페이지 모델 속성을 지정합니다.Specify a page model property for the class that includes an IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

참고

IFormFile은 작업 메서드 매개 변수 또는 바운딩 모델 속성으로 직접 사용할 수 있습니다.IFormFile can be used directly as an action method parameter or as a bound model property. 위 예제에서는 바인딩된 모델 속성을 사용합니다.The prior example uses a bound model property.

FileUpload Pages 폼에 사용 됩니다 Razor .The FileUpload is used in the Razor Pages form:

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

양식이 서버에 POST되면 IFormFile을 스트림으로 복사하여 데이터베이스에 바이트 배열로 저장합니다.When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. 다음 예제에서 _dbContext는 앱의 데이터베이스 컨텍스트를 저장합니다.In the following example, _dbContext stores the app's database context:

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

위의 예제는 샘플 앱에서 보여 주는 시나리오와 비슷합니다.The preceding example is similar to a scenario demonstrated in the sample app:

  • Pages/BufferedSingleFileUploadDb.cshtmlPages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.csPages/BufferedSingleFileUploadDb.cshtml.cs

경고

관계형 데이터베이스에 이진 데이터를 저장할 경우 성능에 나쁜 영향을 줄 수 있으므로 주의하세요.Use caution when storing binary data in relational databases, as it can adversely impact performance.

유효성 검사 없이 IFormFileFileName 속성을 의존하거나 신뢰하지 마세요.Don't rely on or trust the FileName property of IFormFile without validation. FileName 속성은 반드시 HTML 인코딩 후 표시 목적으로만 사용해야 합니다.The FileName property should only be used for display purposes and only after HTML encoding.

제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

스트리밍을 사용하여 대용량 파일 업로드Upload large files with streaming

다음 예제에서는 JavaScript를 사용하여 컨트롤러 작업에 파일을 스트리밍하는 방법을 보여 줍니다.The following example demonstrates how to use JavaScript to stream a file to a controller action. 사용자 지정 필터 특성을 사용하여 파일의 위조 방지 토큰이 생성되고 요청 본문 대신 클라이언트 HTTP 헤더에 전달됩니다.The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. 작업 메서드에서 업로드된 데이터를 직접 처리하므로 다른 사용자 지정 필터에서 형식 모델 바인딩을 사용할 수 없습니다.Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. 작업 내에서 양식의 콘텐츠는 각 개별 MultipartSection을 읽고 적절하게 파일을 처리하거나 콘텐츠를 저장하는 MultipartReader를 사용하여 읽습니다.Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 다중 파트 섹션을 읽은 후 작업에서 자체 모델 바인딩을 수행합니다.After the multipart sections are read, the action performs its own model binding.

초기 페이지 응답은 양식을 로드 하 고 특성을 통해의 위조 방지 토큰을에 저장 합니다 cookie GenerateAntiforgeryTokenCookieAttribute .The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute attribute). 특성은 ASP.NET Core의 기본 제공 위조 방지 지원 기능 을 사용 하 여 cookie 요청 토큰으로를 설정 합니다.The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:

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은 모델 바인딩을 사용하지 않도록 설정하는 데 사용됩니다.The DisableFormValueModelBindingAttribute is used to disable model binding:

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

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

샘플 앱에서 GenerateAntiforgeryTokenCookieAttribute 및는 페이지 DisableFormValueModelBindingAttribute /StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices Razor 규칙을 사용 하 여 및의 페이지 응용 프로그램 모델에 필터로 적용 됩니다.In the sample app, GenerateAntiforgeryTokenCookieAttribute and DisableFormValueModelBindingAttribute are applied as filters to the page application models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in Startup.ConfigureServices using Razor Pages conventions:

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

모델 바인딩은 양식을 읽지 않으므로 양식에서 바인딩된 매개 변수가 바인딩되지 않습니다(쿼리, 경로 및 헤더는 계속 작동함).Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). 작업 메서드는 Request 속성으로 직접 작동합니다.The action method works directly with the Request property. MultipartReader는 각 섹션을 읽는 데 사용됩니다.A MultipartReader is used to read each section. 키/값 데이터는 KeyValueAccumulator에 저장됩니다.Key/value data is stored in a KeyValueAccumulator. 다중 파트 섹션을 읽은 후 KeyValueAccumulator의 내용이 양식 데이터를 모델 형식으로 바인딩하는 데 사용됩니다.After the multipart sections are read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

EF Core를 사용하여 데이터베이스에 스트리밍하기 위한 전체 StreamingController.UploadDatabase 메서드는 다음과 같습니다.The complete StreamingController.UploadDatabase method for streaming to a database with 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 = new byte[0];

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

실제 위치로 스트리밍하기 위한 전체 StreamingController.UploadPhysical 메서드는 다음과 같습니다.The complete StreamingController.UploadPhysical method for streaming to a physical location:

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

샘플 앱에서 유효성 검사는 FileHelpers.ProcessStreamedFile에 의해 처리됩니다.In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile.

유효성 검사Validation

샘플 앱의 FileHelpers 클래스는 버퍼링된 IFormFile 및 스트리밍된 파일 업로드에 대한 여러 검사를 보여 줍니다.The sample app's FileHelpers class demonstrates a several checks for buffered IFormFile and streamed file uploads. 샘플 앱에서 IFormFile 버퍼링된 파일 업로드를 처리하려면 Utilities/FileHelpers.cs 파일에서 ProcessFormFile 메서드를 참조하세요.For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. 스트리밍된 파일의 처리는 동일한 파일의 ProcessStreamedFile 메서드를 참조하세요.For processing streamed files, see the ProcessStreamedFile method in the same file.

경고

샘플 앱에서 보여 주는 유효성 검사 처리 메서드는 업로드된 파일의 내용을 검사하지 않습니다.The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. 대부분의 프로덕션 시나리오에서는 사용자 또는 다른 시스템에서 파일을 사용할 수 있도록 하기 전에 파일에 바이러스/맬웨어 스캐너 API를 사용합니다.In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

항목 샘플에서는 유효성 검사 기술에 대한 작업 예제를 제공하지만, 다음과 같은 경우가 아니면 프로덕션 앱에서 FileHelpers 클래스를 구현하지 마세요.Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers class in a production app unless you:

  • 구현을 완전히 이해합니다.Fully understand the implementation.
  • 앱의 환경 및 사양에 맞게 구현을 수정합니다.Modify the implementation as appropriate for the app's environment and specifications.

이러한 요구 사항을 해결하지 않고 앱에서 보안 코드를 무분별하게 구현해서는 안 됩니다.Never indiscriminately implement security code in an app without addressing these requirements.

콘텐츠 유효성 검사Content validation

업로드된 콘텐츠에 타사 바이러스/맬웨어 검사 API를 사용합니다.Use a third party virus/malware scanning API on uploaded content.

대용량 시나리오에서 파일 검사는 많은 서버 리소스를 요구합니다.Scanning files is demanding on server resources in high volume scenarios. 파일 검사로 인해 요청 처리 성능이 저하된 경우 검사 작업을 백그라운드 서비스로 오프로드하는 것이 좋습니다.이 경우 앱 서버와 다른 서버에서 실행되는 서비스가 있을 수 있습니다.If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. 일반적으로 업로드된 파일은 백그라운드 바이러스 검사 프로그램에서 검사될 때까지 격리된 영역에 저장됩니다.Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. 파일이 전달되면 파일이 일반 파일 스토리지 위치로 이동됩니다.When a file passes, the file is moved to the normal file storage location. 이러한 단계는 일반적으로 파일의 검사 상태를 나타내는 데이터베이스 레코드와 함께 수행됩니다.These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. 이러한 방법을 사용하여 앱 및 앱 서버는 요청에 응답하는 데 집중합니다.By using such an approach, the app and app server remain focused on responding to requests.

파일 확장명 유효성 검사File extension validation

업로드된 파일의 확장명을 허용된 확장명 목록에 따라 확인해야 합니다.The uploaded file's extension should be checked against a list of permitted extensions. 예를 들면 다음과 같습니다.For example:

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
}

파일 서명 유효성 검사File signature validation

파일의 서명은 파일 시작 부분의 처음 몇 바이트에 의해 결정됩니다.A file's signature is determined by the first few bytes at the start of a file. 이러한 바이트는 확장명이 파일 내용과 일치하는지 여부를 나타내는 데 사용할 수 있습니다.These bytes can be used to indicate if the extension matches the content of the file. 샘플 앱은 몇 가지 일반적인 파일 형식에 대한 파일 서명을 확인합니다.The sample app checks file signatures for a few common file types. 다음 예제에서는 파일에서 JPEG 이미지 파일에 대한 서명을 확인합니다.In the following example, the file signature for a JPEG image is checked against the 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));
}

추가 파일 서명을 얻으려면 파일 서명 데이터베이스 및 공식 파일 사양을 참조하세요.To obtain additional file signatures, see the File Signatures Database and official file specifications.

파일 이름 보안File name security

실제 스토리지에 파일을 저장하는 데 클라이언트에서 제공하는 파일 이름을 사용하지 마세요.Never use a client-supplied file name for saving a file to physical storage. Path.GetRandomFileName 또는 Path.GetTempFileName을 사용하여 임시 스토리지에 대한 전체 경로(파일 이름을 포함)를 만들어 파일에 대한 안전한 파일 이름을 만듭니다.Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.

Razor 자동으로 표시 하기 위해 속성 값을 인코딩합니다.Razor automatically HTML encodes property values for display. 다음 코드는 안전하게 사용할 수 있습니다.The following code is safe to use:

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

외부 Razor 에서는 항상 HtmlEncode 사용자 요청의 파일 이름 콘텐츠를 사용 합니다.Outside of Razor, always HtmlEncode file name content from a user's request.

많은 구현에서 파일 존재 여부에 대한 확인이 포함되어야 합니다. 그렇지 않으면 파일이 같은 이름의 파일을 덮어씁니다.Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. 앱의 사양을 충족하는 추가 논리를 제공합니다.Supply additional logic to meet your app's specifications.

크기 유효성 검사Size validation

업로드된 파일의 크기를 제한합니다.Limit the size of uploaded files.

샘플 앱에서 파일 크기는 2MB(바이트 단위로 표시)로 제한됩니다.In the sample app, the size of the file is limited to 2 MB (indicated in bytes). 이 제한은 파일의 구성을 통해 제공 됩니다 appsettings.json .The limit is supplied via Configuration from the appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel 클래스에 삽입됩니다.The FileSizeLimit is injected into PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

파일 크기가 제한을 초과하는 파일은 거부됩니다.When a file size exceeds the limit, the file is rejected:

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

이름 특성 값을 POST 메서드의 매개 변수 이름과 일치Match name attribute value to parameter name of POST method

폼 데이터를 Razor 게시 하거나 JavaScript를 직접 사용 하는 형식이 아닌 FormData 경우 폼의 요소에 지정 된 이름이 나 FormData 컨트롤러 작업의 매개 변수 이름과 일치 해야 합니다.In non-Razor forms that POST form data or use JavaScript's FormData directly, the name specified in the form's element or FormData must match the name of the parameter in the controller's action.

다음 예제에서는In the following example:

  • <input> 요소를 사용하는 경우 name 특성은 값 battlePlans로 설정됩니다.When using an <input> element, the name attribute is set to the value battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript에서 FormData를 사용하는 경우 이름이 값 battlePlans로 설정됩니다.When using FormData in JavaScript, the name is set to the value battlePlans:

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

C# 메서드(battlePlans)의 매개 변수와 일치하는 이름을 사용합니다.Use a matching name for the parameter of the C# method (battlePlans):

  • Razor페이지 페이지 처리기 메서드를 Upload 다음과 같이 지정 합니다.For a Razor Pages page handler method named Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST 컨트롤러 작업 메서드:For an MVC POST controller action method:

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

서버 및 앱 구성Server and app configuration

다중 파트 본문 길이 제한Multipart body length limit

MultipartBodyLengthLimit에서 각 다중 파트 본문의 길이에 대한 제한을 설정합니다.MultipartBodyLengthLimit sets the limit for the length of each multipart body. 양식 섹션이 이 제한을 초과하면 구문 분석할 때 InvalidDataException이 throw됩니다.Form sections that exceed this limit throw an InvalidDataException when parsed. 기본값은 134,217,728(128MB)입니다.The default is 134,217,728 (128 MB). Startup.ConfigureServices에서 MultipartBodyLengthLimit 설정을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices:

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

RequestFormLimitsAttribute는 단일 페이지 또는 작업에 대해 MultipartBodyLengthLimit을 설정하는 데 사용됩니다.RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

페이지 Razor 앱 또는 MVC 앱에서 필터를 페이지 모델 또는 작업 메서드에 적용 합니다.In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

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

Kestrel 최대 요청 본문 크기Kestrel maximum request body size

Kestrel에서 호스트되는 앱의 경우 기본 최대 요청 본문 크기는 30,000,000바이트(약 28.6MB)입니다.For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. MaxRequestBodySize Kestrel 서버 옵션을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MaxRequestBodySize Kestrel server option:

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는 단일 페이지 또는 작업에 대해 MaxRequestBodySize를 설정하는 데 사용됩니다.RequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

페이지 Razor 앱 또는 MVC 앱에서 필터를 페이지 처리기 클래스 또는 작업 메서드에 적용 합니다.In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

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

RequestSizeLimitAttribute지시문을 사용 하 여를 적용할 수도 있습니다 @attribute Razor .The RequestSizeLimitAttribute can also be applied using the @attribute Razor directive:

@attribute [RequestSizeLimitAttribute(52428800)]

기타 Kestrel 제한Other Kestrel limits

다른 Kestrel 제한이 Kestrel에서 호스트되는 앱에 적용될 수 있습니다.Other Kestrel limits may apply for apps hosted by Kestrel:

IISIIS

기본 요청 제한 ( maxAllowedContentLength )은 3000만 바이트 이며 약 28.6 MB입니다.The default request limit (maxAllowedContentLength) is 30,000,000 bytes, which is approximately 28.6 MB. 파일의 제한을 사용자 지정 합니다 web.config .Customize the limit in the web.config file. 다음 예제에서 제한은 50 (52428800 바이트)로 설정 됩니다.In the following example, the limit is set to 50 MB (52,428,800 bytes):

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

maxAllowedContentLength설정은 IIS에만 적용 됩니다.The maxAllowedContentLength setting only applies to IIS. 자세한 내용은 요청 제한 <requestLimits> 을 참조 하세요.For more information, see Request Limits <requestLimits>.

문제 해결Troubleshoot

다음은 파일 업로드 및 가능한 솔루션을 사용하여 작업할 때 자주 발생하는 몇 가지 일반적인 문제입니다.Below are some common problems encountered when working with uploading files and their possible solutions.

IIS 서버에 배포할 때 찾을 수 없음 오류Not Found error when deployed to an IIS server

다음 오류는 업로드된 파일이 서버의 구성된 콘텐츠 길이를 초과했음을 나타냅니다.The following error indicates that the uploaded file exceeds the server's configured content length:

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

자세한 내용은 IIS 섹션을 참조하세요.For more information, see the IIS section.

연결 실패Connection failure

연결 오류 및 서버 연결 다시 설정은 업로드된 파일이 Kestrel의 최대 요청 본문 크기를 초과함을 나타낼 수 있습니다.A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. 자세한 내용은 Kestrel 최대 요청 본문 크기 섹션을 참조하세요.For more information, see the Kestrel maximum request body size section. Kestrel 클라이언트 연결 제한을 조정해야 할 수도 있습니다.Kestrel client connection limits may also require adjustment.

IFormFile 사용 시 Null 참조 예외Null Reference Exception with IFormFile

컨트롤러에서 IFormFile을 사용하여 업로드된 파일을 수락하지만 값이 null이면 HTML 양식에서 multipart/form-dataenctype 값을 지정하는지 확인합니다.If the controller is accepting uploaded files using IFormFile but the value is null, confirm that the HTML form is specifying an enctype value of multipart/form-data. <form> 요소에서 이 특성이 설정되지 않으면 파일 업로드가 실행되지 않고 바인딩된 IFormFile 인수는 모두 null이 됩니다.If this attribute isn't set on the <form> element, the file upload doesn't occur and any bound IFormFile arguments are null. 또한 양식 데이터의 업로드 이름 지정이 앱의 이름 지정과 일치하는지 확인합니다.Also confirm that the upload naming in form data matches the app's naming.

스트림이 너무 깁니다.Stream was too long

이 항목의 예제에서는 업로드된 파일의 콘텐츠를 저장하는 데 MemoryStream에 의존합니다.The examples in this topic rely upon MemoryStream to hold the uploaded file's content. MemoryStream의 크기 제한은 int.MaxValue입니다.The size limit of a MemoryStream is int.MaxValue. 앱의 파일 업로드 시나리오에서 50MB보다 큰 파일 콘텐츠를 보관해야 하는 경우 업로드된 파일의 콘텐츠를 보관할 때 단일 MemoryStream에 의존하지 않는 대체 방법을 사용하세요.If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream for holding an uploaded file's content.

ASP.NET Core는 소용량 파일의 경우에는 버퍼링된 모델 바인딩을 사용하여 하나 이상의 파일을 업로드하고, 대용량 파일의 경우에는 버퍼링되지 않은 스트리밍을 지원합니다.ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

예제 코드 살펴보기 및 다운로드 (다운로드 방법)View or download sample code (how to download)

보안 고려 사항Security considerations

사용자에게 서버에 파일을 업로드하는 기능을 제공할 때는 주의해야 합니다.Use caution when providing users with the ability to upload files to a server. 공격자는 다음을 시도할 수 있습니다.Attackers may attempt to:

  • 서비스 거부 공격을 실행.Execute denial of service attacks.
  • 바이러스 또는 맬웨어를 업로드.Upload viruses or malware.
  • 다른 방법으로 네트워크 및 서버를 손상.Compromise networks and servers in other ways.

공격이 성공할 가능성을 줄이는 보안 단계는 다음과 같습니다.Security steps that reduce the likelihood of a successful attack are:

  • 전용 파일 업로드 영역(바람직하게는 시스템 드라이브가 아닌 위치)에 파일을 업로드합니다.Upload files to a dedicated file upload area, preferably to a non-system drive. 전용 위치를 사용하면 업로드된 파일에 대한 보안 제한을 더 쉽게 적용할 수 있습니다.A dedicated location makes it easier to impose security restrictions on uploaded files. 파일 업로드 위치에 대한 실행 권한을 사용하지 않도록 설정합니다.†Disable execute permissions on the file upload location.†
  • 업로드된 파일을 앱과 동일한 디렉터리 트리에 보관하지 마세요.†Do not persist uploaded files in the same directory tree as the app.†
  • 앱에 의해 결정된 안전한 파일 이름을 사용합니다.Use a safe file name determined by the app. 사용자가 제공한 파일 이름 또는 업로드 된 파일의 신뢰할 수 없는 파일 이름을 사용 하지 마세요. † HTML을 표시 하는 경우 신뢰할 수 없는 파일 이름을 인코딩합니다.Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. 예를 들어 파일 이름을 기록 하거나 UI에 표시 하는 경우 ( Razor 자동으로 HTML 인코딩 출력)For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
  • 앱의 디자인 사양으로 승인된 파일 확장명만 허용합니다.†Allow only approved file extensions for the app's design specification.†
  • 서버에서 클라이언트 쪽 검사가 수행 되는지 확인 합니다. † 클라이언트 쪽 검사는 쉽게 피할 수 있습니다.Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
  • 업로드된 파일의 크기를 확인합니다.Check the size of an uploaded file. 대규모로 업로드되지 않도록 최대 크기 제한을 설정합니다.†Set a maximum size limit to prevent large uploads.†
  • 업로드된 파일이 같은 이름의 파일을 덮어쓰면 안 되는 경우 파일을 업로드하기 전에 데이터베이스 또는 실제 스토리지에서 파일 이름을 확인합니다.When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
  • 파일이 저장되기 전에 업로드된 콘텐츠에 대해 바이러스/맬웨어 스캐너를 실행합니다.Run a virus/malware scanner on uploaded content before the file is stored.

†샘플 앱은 조건을 충족하는 방법을 보여 줍니다.†The sample app demonstrates an approach that meets the criteria.

경고

시스템에 악성 코드를 업로드하는 행위는 흔히 다음을 수행할 수 있는 코드를 실행하기 위한 첫 단계가 됩니다.Uploading malicious code to a system is frequently the first step to executing code that can:

  • 시스템을 완전히 제어합니다.Completely gain control of a system.
  • 시스템 작동이 중단되는 결과로 시스템을 오버로드합니다.Overload a system with the result that the system crashes.
  • 사용자 또는 시스템 데이터를 손상시킵니다.Compromise user or system data.
  • 공용 UI에 그래피티를 적용합니다.Apply graffiti to a public UI.

사용자의 파일을 수락할 때 공격 노출 영역을 줄이는 방법에 대한 자세한 내용은 다음 리소스를 참조하세요.For information on reducing the attack surface area when accepting files from users, see the following resources:

샘플 앱의 예제를 포함하여 보안 조치를 구현하는 방법에 대한 자세한 내용은 유효성 검사 섹션을 참조하세요.For more information on implementing security measures, including examples from the sample app, see the Validation section.

스토리지 시나리오Storage scenarios

파일에 대한 일반적인 스토리지 옵션은 다음과 같습니다.Common storage options for files include:

  • 데이터베이스Database

    • 소용량 파일 업로드의 경우 데이터베이스는 실제 스토리지(파일 시스템 또는 네트워크 공유) 옵션보다 빠른 경우가 많습니다.For small file uploads, a database is often faster than physical storage (file system or network share) options.
    • 사용자 데이터에 대한 데이터베이스 레코드를 검색할 때 파일 콘텐츠(예: 아바타 이미지)를 동시에 제공할 수 있으므로 데이터베이스는 실제 스토리지 옵션보다 편리합니다.A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
    • 데이터베이스는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.A database is potentially less expensive than using a data storage service.
  • 실제 스토리지(파일 시스템 또는 네트워크 공유)Physical storage (file system or network share)

    • 대용량 파일 업로드의 경우:For large file uploads:
      • 데이터베이스 한도 때문에 업로드 크기가 제한될 수 있습니다.Database limits may restrict the size of the upload.
      • 실제 스토리지는 데이터베이스 스토리지보다 경제적이지 않은 경우가 자주 있습니다.Physical storage is often less economical than storage in a database.
    • 실제 스토리지는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.Physical storage is potentially less expensive than using a data storage service.
    • 앱의 프로세스에는 스토리지 위치에 대한 읽기 및 쓰기 권한이 있어야 합니다.The app's process must have read and write permissions to the storage location. 실행 권한을 부여하지 마세요.Never grant execute permission.
  • 데이터 스토리지 서비스(예: Azure Blob Storage)Data storage service (for example, Azure Blob Storage)

    • 서비스는 일반적으로 단일 실패 지점에 노출되는 온-프레미스 솔루션에 비해 향상된 확장성 및 복원력을 제공합니다.Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
    • 서비스는 대용량 스토리지 인프라 시나리오에서 비용이 더 저렴할 수 있습니다.Services are potentially lower cost in large storage infrastructure scenarios.

    자세한 내용은 빠른 시작: .net을 사용 하 여 개체 저장소에 blob 만들기를 참조 하세요.For more information, see Quickstart: Use .NET to create a blob in object storage.

파일 업로드 시나리오File upload scenarios

파일 업로드를 위한 일반적인 방법 두 가지는 버퍼링 및 스트리밍입니다.Two general approaches for uploading files are buffering and streaming.

버퍼링Buffering

전체 파일을 파일 처리 또는 저장에 사용되는 파일의 C# 표현인 IFormFile로 읽어 들입니다.The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

파일 업로드에서 사용되는 리소스(디스크, 메모리)는 동시 파일 업로드 크기와 수에 따라 달라집니다.The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. 앱이 너무 많은 업로드를 버퍼링하려 할 경우 메모리 또는 디스크 공간이 부족하면 사이트의 작동이 중단됩니다.If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. 파일 업로드의 크기 또는 빈도로 인해 앱 리소스가 소진되는 경우 스트리밍을 사용합니다.If the size or frequency of file uploads is exhausting app resources, use streaming.

참고

버퍼링된 단일 파일이 64KB를 초과하는 경우 메모리에서 디스크의 임시 파일로 이동됩니다.Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

소용량 파일 버퍼링은 이 항목의 다음 섹션에서 설명합니다.Buffering small files is covered in the following sections of this topic:

스트리밍Streaming

파일은 다중 파트 요청에서 수신되며 앱에서 직접 처리하거나 저장합니다.The file is received from a multipart request and directly processed or saved by the app. 스트리밍은 성능을 크게 개선하지 않습니다.Streaming doesn't improve performance significantly. 스트리밍을 통해 파일을 업로드하면 메모리 또는 디스크 공간에 대한 요구가 줄어듭니다.Streaming reduces the demands for memory or disk space when uploading files.

대용량 파일 스트리밍은 스트리밍을 사용하여 대용량 파일 업로드 섹션에서 설명합니다.Streaming large files is covered in the Upload large files with streaming section.

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 실제 스토리지에 업로드Upload small files with buffered model binding to physical storage

소용량 파일을 업로드하려면 다중 파트 양식을 사용하거나 JavaScript를 사용하여 POST 요청을 생성합니다.To upload small files, use a multipart form or construct a POST request using JavaScript.

다음 예제에서는 Razor pages 폼을 사용 하 여 단일 파일 (샘플 응용 프로그램의 Pages/BufferedSingleFileUploadPhysical )을 업로드 하는 방법을 보여 줍니다.The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

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

다음 예제는 이전 예제와 비슷하지만 다음과 같은 차이가 있습니다.The following example is analogous to the prior example except that:

  • 양식의 데이터를 제출하는 데 JavaScript(Fetch API)가 사용됩니다.JavaScript's (Fetch API) is used to submit the form's data.
  • 유효성 검사를 수행하지 않습니다.There's no validation.
<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>

Fetch API를 지원하지 않는 클라이언트에 대해 JavaScript로 양식 POST를 수행하려면 다음 방법 중 하나를 사용합니다.To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:

  • Fetch Polyfill(예: window.fetch polyfill (github/fetch))을 사용합니다.Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).

  • XMLHttpRequest을 사용하세요.Use XMLHttpRequest. 예를 들면 다음과 같습니다.For example:

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

파일 업로드를 지원하려면 HTML 양식에서 multipart/form-data의 인코딩 유형(enctype)을 지정해야 합니다.In order to support file uploads, HTML forms must specify an encoding type (enctype) of multipart/form-data.

files 입력 요소가 다중 파일 업로드를 지원하려면 <input> 요소에 multiple 특성을 제공합니다.For a files input element to support uploading multiple files provide the multiple attribute on the <input> element:

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

서버에 업로드된 개별 파일은 IFormFile을 사용하여 모델 바인딩을 통해 액세스할 수 있습니다.The individual files uploaded to the server can be accessed through Model Binding using IFormFile. 샘플 앱은 데이터베이스 및 실제 스토리지 시나리오에 대한 다중 버퍼링된 파일 업로드를 보여 줍니다.The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

경고

표시 및 로깅 이외에는 IFormFileFileName 속성을 사용하지 마세요.Do not use the FileName property of IFormFile other than for display and logging. 표시하거나 로깅할 경우 파일 이름을 HTML로 인코딩합니다.When displaying or logging, HTML encode the file name. 공격자는 전체 경로나 상대 경로를 포함하여 악의적인 파일 이름을 제공할 수 있습니다.An attacker can provide a malicious filename, including full paths or relative paths. 애플리케이션에서 다음을 수행해야 합니다.Applications should:

  • 사용자가 제공한 파일 이름에서 경로를 제거합니다.Remove the path from the user-supplied filename.
  • UI 또는 로깅을 위해 HTML로 인코딩되고 경로가 제거된 파일 이름을 저장합니다.Save the HTML-encoded, path-removed filename for UI or logging.
  • 스토리지의 새 임의 파일 이름을 생성합니다.Generate a new random filename for storage.

다음 코드는 파일 이름에서 경로를 제거합니다.The following code removes the path from the file name:

string untrustedFileName = Path.GetFileName(pathName);

지금까지 제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided thus far don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

모델 바인딩 및 IFormFile을 사용하여 파일을 업로드하는 경우 작업 메서드에서 다음을 허용할 수 있습니다.When uploading files using model binding and IFormFile, the action method can accept:

참고

바인딩은 이름을 기준으로 양식 파일을 일치시킵니다.Binding matches form files by name. 예를 들어 <input type="file" name="formFile">의 HTML name 값은 바인딩된 C# 매개 변수/속성(FormFile)과 일치해야 합니다.For example, the HTML name value in <input type="file" name="formFile"> must match the C# parameter/property bound (FormFile). 자세한 내용은 이름 특성 값을 POST 메서드의 매개 변수 이름과 일치 섹션을 참조하세요.For more information, see the Match name attribute value to parameter name of POST method section.

다음 예제가 하는 일:The following example:

  • 하나 이상의 업로드된 파일을 반복합니다.Loops through one or more uploaded files.
  • Path.GetTempFileName을 사용하여 파일 이름을 포함하는 파일 전체 경로를 반환합니다.Uses Path.GetTempFileName to return a full path for a file, including the file name.
  • 앱에서 생성한 파일 이름을 사용하여 로컬 파일 시스템에 파일을 저장합니다.Saves the files to the local file system using a file name generated by the app.
  • 업로드된 파일의 총 수와 크기를 반환합니다.Returns the total number and size of files uploaded.
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 });
}

Path.GetRandomFileName을 사용하여 경로 없이 파일 이름을 생성합니다.Use Path.GetRandomFileName to generate a file name without a path. 다음 예제에서는 경로를 구성에서 가져옵니다.In the following example, the path is obtained from configuration:

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

에 전달된 경로는 FileStream 반드시 파일 이름을 포함해야 합니다.The path passed to the FileStream must include the file name. 파일 이름을 제공하지 않으면 런타임에 UnauthorizedAccessException이 발생합니다.If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.

IFormFile 기술을 사용하여 업로드된 파일은 처리 전에 서버의 메모리나 디스크에 버퍼링됩니다.Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. 작업 메서드 내부에서 IFormFile 내용을 Stream으로 액세스할 수 있습니다.Inside the action method, the IFormFile contents are accessible as a Stream. 로컬 파일 시스템 외에도 파일을 네트워크 공유 또는 파일 스토리지 서비스(예: Azure Blob Storage)에 저장할 수 있습니다.In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.

업로드를 위해 여러 파일에 대해 루프를 실행하고 안전한 파일 이름을 사용하는 또 하나의 예제는 샘플 앱에서 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 를 참조하세요.For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

경고

이전 임시 파일을 삭제하지 않고 65,535개를 초과하는 파일을 만들면 Path.GetTempFileNameIOException을 throw합니다.Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. 65535개 파일 제한은 서버당 제한입니다.The limit of 65,535 files is a per-server limit. Windows OS에서 이 제한에 대한 자세한 내용은 다음 항목의 설명을 참조하세요.For more information on this limit on Windows OS, see the remarks in the following topics:

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 데이터베이스에 업로드Upload small files with buffered model binding to a database

Entity Framework를 사용하여 데이터베이스에 이진 파일 데이터를 저장하려면 엔터티에서 Byte 배열 속성을 정의합니다.To store binary file data in a database using Entity Framework, define a Byte array property on the entity:

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

IFormFile을 포함하는 클래스에 대한 페이지 모델 속성을 지정합니다.Specify a page model property for the class that includes an IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

참고

IFormFile은 작업 메서드 매개 변수 또는 바운딩 모델 속성으로 직접 사용할 수 있습니다.IFormFile can be used directly as an action method parameter or as a bound model property. 위 예제에서는 바인딩된 모델 속성을 사용합니다.The prior example uses a bound model property.

FileUpload Pages 폼에 사용 됩니다 Razor .The FileUpload is used in the Razor Pages form:

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

양식이 서버에 POST되면 IFormFile을 스트림으로 복사하여 데이터베이스에 바이트 배열로 저장합니다.When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. 다음 예제에서 _dbContext는 앱의 데이터베이스 컨텍스트를 저장합니다.In the following example, _dbContext stores the app's database context:

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

위의 예제는 샘플 앱에서 보여 주는 시나리오와 비슷합니다.The preceding example is similar to a scenario demonstrated in the sample app:

  • Pages/BufferedSingleFileUploadDb.cshtmlPages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.csPages/BufferedSingleFileUploadDb.cshtml.cs

경고

관계형 데이터베이스에 이진 데이터를 저장할 경우 성능에 나쁜 영향을 줄 수 있으므로 주의하세요.Use caution when storing binary data in relational databases, as it can adversely impact performance.

유효성 검사 없이 IFormFileFileName 속성을 의존하거나 신뢰하지 마세요.Don't rely on or trust the FileName property of IFormFile without validation. FileName 속성은 반드시 HTML 인코딩 후 표시 목적으로만 사용해야 합니다.The FileName property should only be used for display purposes and only after HTML encoding.

제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

스트리밍을 사용하여 대용량 파일 업로드Upload large files with streaming

다음 예제에서는 JavaScript를 사용하여 컨트롤러 작업에 파일을 스트리밍하는 방법을 보여 줍니다.The following example demonstrates how to use JavaScript to stream a file to a controller action. 사용자 지정 필터 특성을 사용하여 파일의 위조 방지 토큰이 생성되고 요청 본문 대신 클라이언트 HTTP 헤더에 전달됩니다.The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. 작업 메서드에서 업로드된 데이터를 직접 처리하므로 다른 사용자 지정 필터에서 형식 모델 바인딩을 사용할 수 없습니다.Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. 작업 내에서 양식의 콘텐츠는 각 개별 MultipartSection을 읽고 적절하게 파일을 처리하거나 콘텐츠를 저장하는 MultipartReader를 사용하여 읽습니다.Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 다중 파트 섹션을 읽은 후 작업에서 자체 모델 바인딩을 수행합니다.After the multipart sections are read, the action performs its own model binding.

초기 페이지 응답은 양식을 로드 하 고 특성을 통해의 위조 방지 토큰을에 저장 합니다 cookie GenerateAntiforgeryTokenCookieAttribute .The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute attribute). 특성은 ASP.NET Core의 기본 제공 위조 방지 지원 기능 을 사용 하 여 cookie 요청 토큰으로를 설정 합니다.The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:

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은 모델 바인딩을 사용하지 않도록 설정하는 데 사용됩니다.The DisableFormValueModelBindingAttribute is used to disable model binding:

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

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

샘플 앱에서 GenerateAntiforgeryTokenCookieAttribute 및는 페이지 DisableFormValueModelBindingAttribute /StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices Razor 규칙을 사용 하 여 및의 페이지 응용 프로그램 모델에 필터로 적용 됩니다.In the sample app, GenerateAntiforgeryTokenCookieAttribute and DisableFormValueModelBindingAttribute are applied as filters to the page application models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in Startup.ConfigureServices using Razor Pages conventions:

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

모델 바인딩은 양식을 읽지 않으므로 양식에서 바인딩된 매개 변수가 바인딩되지 않습니다(쿼리, 경로 및 헤더는 계속 작동함).Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). 작업 메서드는 Request 속성으로 직접 작동합니다.The action method works directly with the Request property. MultipartReader는 각 섹션을 읽는 데 사용됩니다.A MultipartReader is used to read each section. 키/값 데이터는 KeyValueAccumulator에 저장됩니다.Key/value data is stored in a KeyValueAccumulator. 다중 파트 섹션을 읽은 후 KeyValueAccumulator의 내용이 양식 데이터를 모델 형식으로 바인딩하는 데 사용됩니다.After the multipart sections are read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

EF Core를 사용하여 데이터베이스에 스트리밍하기 위한 전체 StreamingController.UploadDatabase 메서드는 다음과 같습니다.The complete StreamingController.UploadDatabase method for streaming to a database with 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 = new byte[0];

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

실제 위치로 스트리밍하기 위한 전체 StreamingController.UploadPhysical 메서드는 다음과 같습니다.The complete StreamingController.UploadPhysical method for streaming to a physical location:

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

샘플 앱에서 유효성 검사는 FileHelpers.ProcessStreamedFile에 의해 처리됩니다.In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile.

유효성 검사Validation

샘플 앱의 FileHelpers 클래스는 버퍼링된 IFormFile 및 스트리밍된 파일 업로드에 대한 여러 검사를 보여 줍니다.The sample app's FileHelpers class demonstrates a several checks for buffered IFormFile and streamed file uploads. 샘플 앱에서 IFormFile 버퍼링된 파일 업로드를 처리하려면 Utilities/FileHelpers.cs 파일에서 ProcessFormFile 메서드를 참조하세요.For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. 스트리밍된 파일의 처리는 동일한 파일의 ProcessStreamedFile 메서드를 참조하세요.For processing streamed files, see the ProcessStreamedFile method in the same file.

경고

샘플 앱에서 보여 주는 유효성 검사 처리 메서드는 업로드된 파일의 내용을 검사하지 않습니다.The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. 대부분의 프로덕션 시나리오에서는 사용자 또는 다른 시스템에서 파일을 사용할 수 있도록 하기 전에 파일에 바이러스/맬웨어 스캐너 API를 사용합니다.In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

항목 샘플에서는 유효성 검사 기술에 대한 작업 예제를 제공하지만, 다음과 같은 경우가 아니면 프로덕션 앱에서 FileHelpers 클래스를 구현하지 마세요.Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers class in a production app unless you:

  • 구현을 완전히 이해합니다.Fully understand the implementation.
  • 앱의 환경 및 사양에 맞게 구현을 수정합니다.Modify the implementation as appropriate for the app's environment and specifications.

이러한 요구 사항을 해결하지 않고 앱에서 보안 코드를 무분별하게 구현해서는 안 됩니다.Never indiscriminately implement security code in an app without addressing these requirements.

콘텐츠 유효성 검사Content validation

업로드된 콘텐츠에 타사 바이러스/맬웨어 검사 API를 사용합니다.Use a third party virus/malware scanning API on uploaded content.

대용량 시나리오에서 파일 검사는 많은 서버 리소스를 요구합니다.Scanning files is demanding on server resources in high volume scenarios. 파일 검사로 인해 요청 처리 성능이 저하된 경우 검사 작업을 백그라운드 서비스로 오프로드하는 것이 좋습니다.이 경우 앱 서버와 다른 서버에서 실행되는 서비스가 있을 수 있습니다.If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. 일반적으로 업로드된 파일은 백그라운드 바이러스 검사 프로그램에서 검사될 때까지 격리된 영역에 저장됩니다.Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. 파일이 전달되면 파일이 일반 파일 스토리지 위치로 이동됩니다.When a file passes, the file is moved to the normal file storage location. 이러한 단계는 일반적으로 파일의 검사 상태를 나타내는 데이터베이스 레코드와 함께 수행됩니다.These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. 이러한 방법을 사용하여 앱 및 앱 서버는 요청에 응답하는 데 집중합니다.By using such an approach, the app and app server remain focused on responding to requests.

파일 확장명 유효성 검사File extension validation

업로드된 파일의 확장명을 허용된 확장명 목록에 따라 확인해야 합니다.The uploaded file's extension should be checked against a list of permitted extensions. 예를 들면 다음과 같습니다.For example:

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
}

파일 서명 유효성 검사File signature validation

파일의 서명은 파일 시작 부분의 처음 몇 바이트에 의해 결정됩니다.A file's signature is determined by the first few bytes at the start of a file. 이러한 바이트는 확장명이 파일 내용과 일치하는지 여부를 나타내는 데 사용할 수 있습니다.These bytes can be used to indicate if the extension matches the content of the file. 샘플 앱은 몇 가지 일반적인 파일 형식에 대한 파일 서명을 확인합니다.The sample app checks file signatures for a few common file types. 다음 예제에서는 파일에서 JPEG 이미지 파일에 대한 서명을 확인합니다.In the following example, the file signature for a JPEG image is checked against the 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));
}

추가 파일 서명을 얻으려면 파일 서명 데이터베이스 및 공식 파일 사양을 참조하세요.To obtain additional file signatures, see the File Signatures Database and official file specifications.

파일 이름 보안File name security

실제 스토리지에 파일을 저장하는 데 클라이언트에서 제공하는 파일 이름을 사용하지 마세요.Never use a client-supplied file name for saving a file to physical storage. Path.GetRandomFileName 또는 Path.GetTempFileName을 사용하여 임시 스토리지에 대한 전체 경로(파일 이름을 포함)를 만들어 파일에 대한 안전한 파일 이름을 만듭니다.Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.

Razor 자동으로 표시 하기 위해 속성 값을 인코딩합니다.Razor automatically HTML encodes property values for display. 다음 코드는 안전하게 사용할 수 있습니다.The following code is safe to use:

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

외부 Razor 에서는 항상 HtmlEncode 사용자 요청의 파일 이름 콘텐츠를 사용 합니다.Outside of Razor, always HtmlEncode file name content from a user's request.

많은 구현에서 파일 존재 여부에 대한 확인이 포함되어야 합니다. 그렇지 않으면 파일이 같은 이름의 파일을 덮어씁니다.Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. 앱의 사양을 충족하는 추가 논리를 제공합니다.Supply additional logic to meet your app's specifications.

크기 유효성 검사Size validation

업로드된 파일의 크기를 제한합니다.Limit the size of uploaded files.

샘플 앱에서 파일 크기는 2MB(바이트 단위로 표시)로 제한됩니다.In the sample app, the size of the file is limited to 2 MB (indicated in bytes). 이 제한은 파일의 구성을 통해 제공 됩니다 appsettings.json .The limit is supplied via Configuration from the appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel 클래스에 삽입됩니다.The FileSizeLimit is injected into PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

파일 크기가 제한을 초과하는 파일은 거부됩니다.When a file size exceeds the limit, the file is rejected:

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

이름 특성 값을 POST 메서드의 매개 변수 이름과 일치Match name attribute value to parameter name of POST method

폼 데이터를 Razor 게시 하거나 JavaScript를 직접 사용 하는 형식이 아닌 FormData 경우 폼의 요소에 지정 된 이름이 나 FormData 컨트롤러 작업의 매개 변수 이름과 일치 해야 합니다.In non-Razor forms that POST form data or use JavaScript's FormData directly, the name specified in the form's element or FormData must match the name of the parameter in the controller's action.

다음 예제에서는In the following example:

  • <input> 요소를 사용하는 경우 name 특성은 값 battlePlans로 설정됩니다.When using an <input> element, the name attribute is set to the value battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript에서 FormData를 사용하는 경우 이름이 값 battlePlans로 설정됩니다.When using FormData in JavaScript, the name is set to the value battlePlans:

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

C# 메서드(battlePlans)의 매개 변수와 일치하는 이름을 사용합니다.Use a matching name for the parameter of the C# method (battlePlans):

  • Razor페이지 페이지 처리기 메서드를 Upload 다음과 같이 지정 합니다.For a Razor Pages page handler method named Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST 컨트롤러 작업 메서드:For an MVC POST controller action method:

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

서버 및 앱 구성Server and app configuration

다중 파트 본문 길이 제한Multipart body length limit

MultipartBodyLengthLimit에서 각 다중 파트 본문의 길이에 대한 제한을 설정합니다.MultipartBodyLengthLimit sets the limit for the length of each multipart body. 양식 섹션이 이 제한을 초과하면 구문 분석할 때 InvalidDataException이 throw됩니다.Form sections that exceed this limit throw an InvalidDataException when parsed. 기본값은 134,217,728(128MB)입니다.The default is 134,217,728 (128 MB). Startup.ConfigureServices에서 MultipartBodyLengthLimit 설정을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices:

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

RequestFormLimitsAttribute는 단일 페이지 또는 작업에 대해 MultipartBodyLengthLimit을 설정하는 데 사용됩니다.RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

페이지 Razor 앱 또는 MVC 앱에서 필터를 페이지 모델 또는 작업 메서드에 적용 합니다.In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

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

Kestrel 최대 요청 본문 크기Kestrel maximum request body size

Kestrel에서 호스트되는 앱의 경우 기본 최대 요청 본문 크기는 30,000,000바이트(약 28.6MB)입니다.For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. MaxRequestBodySize Kestrel 서버 옵션을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MaxRequestBodySize Kestrel server option:

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는 단일 페이지 또는 작업에 대해 MaxRequestBodySize를 설정하는 데 사용됩니다.RequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

페이지 Razor 앱 또는 MVC 앱에서 필터를 페이지 처리기 클래스 또는 작업 메서드에 적용 합니다.In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

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

RequestSizeLimitAttribute지시문을 사용 하 여를 적용할 수도 있습니다 @attribute Razor .The RequestSizeLimitAttribute can also be applied using the @attribute Razor directive:

@attribute [RequestSizeLimitAttribute(52428800)]

기타 Kestrel 제한Other Kestrel limits

다른 Kestrel 제한이 Kestrel에서 호스트되는 앱에 적용될 수 있습니다.Other Kestrel limits may apply for apps hosted by Kestrel:

IISIIS

기본 요청 제한 ( maxAllowedContentLength )은 3000만 바이트 이며 약 28.6 MB입니다.The default request limit (maxAllowedContentLength) is 30,000,000 bytes, which is approximately 28.6 MB. 파일의 제한을 사용자 지정 합니다 web.config .Customize the limit in the web.config file. 다음 예제에서 제한은 50 (52428800 바이트)로 설정 됩니다.In the following example, the limit is set to 50 MB (52,428,800 bytes):

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

maxAllowedContentLength설정은 IIS에만 적용 됩니다.The maxAllowedContentLength setting only applies to IIS. 자세한 내용은 요청 제한 <requestLimits> 을 참조 하세요.For more information, see Request Limits <requestLimits>.

에서을 설정 하 여 HTTP 요청에 대 한 최대 요청 본문 크기를 늘립니다 IISServerOptions.MaxRequestBodySize Startup.ConfigureServices .Increase the maximum request body size for the HTTP request by setting IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. 다음 예제에서 제한은 50 (52428800 바이트)로 설정 됩니다.In the following example, the limit is set to 50 MB (52,428,800 bytes):

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

자세한 내용은 IIS가 있는 Windows에서 ASP.NET Core 호스팅를 참조하세요.For more information, see IIS가 있는 Windows에서 ASP.NET Core 호스팅.

문제 해결Troubleshoot

다음은 파일 업로드 및 가능한 솔루션을 사용하여 작업할 때 자주 발생하는 몇 가지 일반적인 문제입니다.Below are some common problems encountered when working with uploading files and their possible solutions.

IIS 서버에 배포할 때 찾을 수 없음 오류Not Found error when deployed to an IIS server

다음 오류는 업로드된 파일이 서버의 구성된 콘텐츠 길이를 초과했음을 나타냅니다.The following error indicates that the uploaded file exceeds the server's configured content length:

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

자세한 내용은 IIS 섹션을 참조하세요.For more information, see the IIS section.

연결 실패Connection failure

연결 오류 및 서버 연결 다시 설정은 업로드된 파일이 Kestrel의 최대 요청 본문 크기를 초과함을 나타낼 수 있습니다.A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. 자세한 내용은 Kestrel 최대 요청 본문 크기 섹션을 참조하세요.For more information, see the Kestrel maximum request body size section. Kestrel 클라이언트 연결 제한을 조정해야 할 수도 있습니다.Kestrel client connection limits may also require adjustment.

IFormFile 사용 시 Null 참조 예외Null Reference Exception with IFormFile

컨트롤러에서 IFormFile을 사용하여 업로드된 파일을 수락하지만 값이 null이면 HTML 양식에서 multipart/form-dataenctype 값을 지정하는지 확인합니다.If the controller is accepting uploaded files using IFormFile but the value is null, confirm that the HTML form is specifying an enctype value of multipart/form-data. <form> 요소에서 이 특성이 설정되지 않으면 파일 업로드가 실행되지 않고 바인딩된 IFormFile 인수는 모두 null이 됩니다.If this attribute isn't set on the <form> element, the file upload doesn't occur and any bound IFormFile arguments are null. 또한 양식 데이터의 업로드 이름 지정이 앱의 이름 지정과 일치하는지 확인합니다.Also confirm that the upload naming in form data matches the app's naming.

스트림이 너무 깁니다.Stream was too long

이 항목의 예제에서는 업로드된 파일의 콘텐츠를 저장하는 데 MemoryStream에 의존합니다.The examples in this topic rely upon MemoryStream to hold the uploaded file's content. MemoryStream의 크기 제한은 int.MaxValue입니다.The size limit of a MemoryStream is int.MaxValue. 앱의 파일 업로드 시나리오에서 50MB보다 큰 파일 콘텐츠를 보관해야 하는 경우 업로드된 파일의 콘텐츠를 보관할 때 단일 MemoryStream에 의존하지 않는 대체 방법을 사용하세요.If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream for holding an uploaded file's content.

ASP.NET Core는 소용량 파일의 경우에는 버퍼링된 모델 바인딩을 사용하여 하나 이상의 파일을 업로드하고, 대용량 파일의 경우에는 버퍼링되지 않은 스트리밍을 지원합니다.ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

예제 코드 살펴보기 및 다운로드 (다운로드 방법)View or download sample code (how to download)

보안 고려 사항Security considerations

사용자에게 서버에 파일을 업로드하는 기능을 제공할 때는 주의해야 합니다.Use caution when providing users with the ability to upload files to a server. 공격자는 다음을 시도할 수 있습니다.Attackers may attempt to:

  • 서비스 거부 공격을 실행.Execute denial of service attacks.
  • 바이러스 또는 맬웨어를 업로드.Upload viruses or malware.
  • 다른 방법으로 네트워크 및 서버를 손상.Compromise networks and servers in other ways.

공격이 성공할 가능성을 줄이는 보안 단계는 다음과 같습니다.Security steps that reduce the likelihood of a successful attack are:

  • 전용 파일 업로드 영역(바람직하게는 시스템 드라이브가 아닌 위치)에 파일을 업로드합니다.Upload files to a dedicated file upload area, preferably to a non-system drive. 전용 위치를 사용하면 업로드된 파일에 대한 보안 제한을 더 쉽게 적용할 수 있습니다.A dedicated location makes it easier to impose security restrictions on uploaded files. 파일 업로드 위치에 대한 실행 권한을 사용하지 않도록 설정합니다.†Disable execute permissions on the file upload location.†
  • 업로드된 파일을 앱과 동일한 디렉터리 트리에 보관하지 마세요.†Do not persist uploaded files in the same directory tree as the app.†
  • 앱에 의해 결정된 안전한 파일 이름을 사용합니다.Use a safe file name determined by the app. 사용자가 제공한 파일 이름 또는 업로드 된 파일의 신뢰할 수 없는 파일 이름을 사용 하지 마세요. † HTML을 표시 하는 경우 신뢰할 수 없는 파일 이름을 인코딩합니다.Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. 예를 들어 파일 이름을 기록 하거나 UI에 표시 하는 경우 ( Razor 자동으로 HTML 인코딩 출력)For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
  • 앱의 디자인 사양으로 승인된 파일 확장명만 허용합니다.†Allow only approved file extensions for the app's design specification.†
  • 서버에서 클라이언트 쪽 검사가 수행 되는지 확인 합니다. † 클라이언트 쪽 검사는 쉽게 피할 수 있습니다.Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
  • 업로드된 파일의 크기를 확인합니다.Check the size of an uploaded file. 대규모로 업로드되지 않도록 최대 크기 제한을 설정합니다.†Set a maximum size limit to prevent large uploads.†
  • 업로드된 파일이 같은 이름의 파일을 덮어쓰면 안 되는 경우 파일을 업로드하기 전에 데이터베이스 또는 실제 스토리지에서 파일 이름을 확인합니다.When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
  • 파일이 저장되기 전에 업로드된 콘텐츠에 대해 바이러스/맬웨어 스캐너를 실행합니다.Run a virus/malware scanner on uploaded content before the file is stored.

†샘플 앱은 조건을 충족하는 방법을 보여 줍니다.†The sample app demonstrates an approach that meets the criteria.

경고

시스템에 악성 코드를 업로드하는 행위는 흔히 다음을 수행할 수 있는 코드를 실행하기 위한 첫 단계가 됩니다.Uploading malicious code to a system is frequently the first step to executing code that can:

  • 시스템을 완전히 제어합니다.Completely gain control of a system.
  • 시스템 작동이 중단되는 결과로 시스템을 오버로드합니다.Overload a system with the result that the system crashes.
  • 사용자 또는 시스템 데이터를 손상시킵니다.Compromise user or system data.
  • 공용 UI에 그래피티를 적용합니다.Apply graffiti to a public UI.

사용자의 파일을 수락할 때 공격 노출 영역을 줄이는 방법에 대한 자세한 내용은 다음 리소스를 참조하세요.For information on reducing the attack surface area when accepting files from users, see the following resources:

샘플 앱의 예제를 포함하여 보안 조치를 구현하는 방법에 대한 자세한 내용은 유효성 검사 섹션을 참조하세요.For more information on implementing security measures, including examples from the sample app, see the Validation section.

스토리지 시나리오Storage scenarios

파일에 대한 일반적인 스토리지 옵션은 다음과 같습니다.Common storage options for files include:

  • 데이터베이스Database

    • 소용량 파일 업로드의 경우 데이터베이스는 실제 스토리지(파일 시스템 또는 네트워크 공유) 옵션보다 빠른 경우가 많습니다.For small file uploads, a database is often faster than physical storage (file system or network share) options.
    • 사용자 데이터에 대한 데이터베이스 레코드를 검색할 때 파일 콘텐츠(예: 아바타 이미지)를 동시에 제공할 수 있으므로 데이터베이스는 실제 스토리지 옵션보다 편리합니다.A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
    • 데이터베이스는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.A database is potentially less expensive than using a data storage service.
  • 실제 스토리지(파일 시스템 또는 네트워크 공유)Physical storage (file system or network share)

    • 대용량 파일 업로드의 경우:For large file uploads:
      • 데이터베이스 한도 때문에 업로드 크기가 제한될 수 있습니다.Database limits may restrict the size of the upload.
      • 실제 스토리지는 데이터베이스 스토리지보다 경제적이지 않은 경우가 자주 있습니다.Physical storage is often less economical than storage in a database.
    • 실제 스토리지는 데이터 스토리지 서비스를 사용하는 것보다 비용이 적게 들 수 있습니다.Physical storage is potentially less expensive than using a data storage service.
    • 앱의 프로세스에는 스토리지 위치에 대한 읽기 및 쓰기 권한이 있어야 합니다.The app's process must have read and write permissions to the storage location. 실행 권한을 부여하지 마세요.Never grant execute permission.
  • 데이터 스토리지 서비스(예: Azure Blob Storage)Data storage service (for example, Azure Blob Storage)

    • 서비스는 일반적으로 단일 실패 지점에 노출되는 온-프레미스 솔루션에 비해 향상된 확장성 및 복원력을 제공합니다.Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
    • 서비스는 대용량 스토리지 인프라 시나리오에서 비용이 더 저렴할 수 있습니다.Services are potentially lower cost in large storage infrastructure scenarios.

    자세한 내용은 빠른 시작: .net을 사용 하 여 개체 저장소에 blob 만들기를 참조 하세요.For more information, see Quickstart: Use .NET to create a blob in object storage. 이 항목에서는 UploadFromFileAsync를 보여 주지만, Stream에서 작업할 때 UploadFromStreamAsync를 사용하여 FileStream을 Blob Storage에 저장할 수 있습니다.The topic demonstrates UploadFromFileAsync, but UploadFromStreamAsync can be used to save a FileStream to blob storage when working with a Stream.

파일 업로드 시나리오File upload scenarios

파일 업로드를 위한 일반적인 방법 두 가지는 버퍼링 및 스트리밍입니다.Two general approaches for uploading files are buffering and streaming.

버퍼링Buffering

전체 파일을 파일 처리 또는 저장에 사용되는 파일의 C# 표현인 IFormFile로 읽어 들입니다.The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

파일 업로드에서 사용되는 리소스(디스크, 메모리)는 동시 파일 업로드 크기와 수에 따라 달라집니다.The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. 앱이 너무 많은 업로드를 버퍼링하려 할 경우 메모리 또는 디스크 공간이 부족하면 사이트의 작동이 중단됩니다.If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. 파일 업로드의 크기 또는 빈도로 인해 앱 리소스가 소진되는 경우 스트리밍을 사용합니다.If the size or frequency of file uploads is exhausting app resources, use streaming.

참고

버퍼링된 단일 파일이 64KB를 초과하는 경우 메모리에서 디스크의 임시 파일로 이동됩니다.Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

소용량 파일 버퍼링은 이 항목의 다음 섹션에서 설명합니다.Buffering small files is covered in the following sections of this topic:

스트리밍Streaming

파일은 다중 파트 요청에서 수신되며 앱에서 직접 처리하거나 저장합니다.The file is received from a multipart request and directly processed or saved by the app. 스트리밍은 성능을 크게 개선하지 않습니다.Streaming doesn't improve performance significantly. 스트리밍을 통해 파일을 업로드하면 메모리 또는 디스크 공간에 대한 요구가 줄어듭니다.Streaming reduces the demands for memory or disk space when uploading files.

대용량 파일 스트리밍은 스트리밍을 사용하여 대용량 파일 업로드 섹션에서 설명합니다.Streaming large files is covered in the Upload large files with streaming section.

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 실제 스토리지에 업로드Upload small files with buffered model binding to physical storage

소용량 파일을 업로드하려면 다중 파트 양식을 사용하거나 JavaScript를 사용하여 POST 요청을 생성합니다.To upload small files, use a multipart form or construct a POST request using JavaScript.

다음 예제에서는 Razor pages 폼을 사용 하 여 단일 파일 (샘플 응용 프로그램의 Pages/BufferedSingleFileUploadPhysical )을 업로드 하는 방법을 보여 줍니다.The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

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

다음 예제는 이전 예제와 비슷하지만 다음과 같은 차이가 있습니다.The following example is analogous to the prior example except that:

  • 양식의 데이터를 제출하는 데 JavaScript(Fetch API)가 사용됩니다.JavaScript's (Fetch API) is used to submit the form's data.
  • 유효성 검사를 수행하지 않습니다.There's no validation.
<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>

Fetch API를 지원하지 않는 클라이언트에 대해 JavaScript로 양식 POST를 수행하려면 다음 방법 중 하나를 사용합니다.To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:

  • Fetch Polyfill(예: window.fetch polyfill (github/fetch))을 사용합니다.Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).

  • XMLHttpRequest을 사용하세요.Use XMLHttpRequest. 예를 들면 다음과 같습니다.For example:

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

파일 업로드를 지원하려면 HTML 양식에서 multipart/form-data의 인코딩 유형(enctype)을 지정해야 합니다.In order to support file uploads, HTML forms must specify an encoding type (enctype) of multipart/form-data.

files 입력 요소가 다중 파일 업로드를 지원하려면 <input> 요소에 multiple 특성을 제공합니다.For a files input element to support uploading multiple files provide the multiple attribute on the <input> element:

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

서버에 업로드된 개별 파일은 IFormFile을 사용하여 모델 바인딩을 통해 액세스할 수 있습니다.The individual files uploaded to the server can be accessed through Model Binding using IFormFile. 샘플 앱은 데이터베이스 및 실제 스토리지 시나리오에 대한 다중 버퍼링된 파일 업로드를 보여 줍니다.The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

경고

표시 및 로깅 이외에는 IFormFileFileName 속성을 사용하지 마세요.Do not use the FileName property of IFormFile other than for display and logging. 표시하거나 로깅할 경우 파일 이름을 HTML로 인코딩합니다.When displaying or logging, HTML encode the file name. 공격자는 전체 경로나 상대 경로를 포함하여 악의적인 파일 이름을 제공할 수 있습니다.An attacker can provide a malicious filename, including full paths or relative paths. 애플리케이션에서 다음을 수행해야 합니다.Applications should:

  • 사용자가 제공한 파일 이름에서 경로를 제거합니다.Remove the path from the user-supplied filename.
  • UI 또는 로깅을 위해 HTML로 인코딩되고 경로가 제거된 파일 이름을 저장합니다.Save the HTML-encoded, path-removed filename for UI or logging.
  • 스토리지의 새 임의 파일 이름을 생성합니다.Generate a new random filename for storage.

다음 코드는 파일 이름에서 경로를 제거합니다.The following code removes the path from the file name:

string untrustedFileName = Path.GetFileName(pathName);

지금까지 제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided thus far don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

모델 바인딩 및 IFormFile을 사용하여 파일을 업로드하는 경우 작업 메서드에서 다음을 허용할 수 있습니다.When uploading files using model binding and IFormFile, the action method can accept:

참고

바인딩은 이름을 기준으로 양식 파일을 일치시킵니다.Binding matches form files by name. 예를 들어 <input type="file" name="formFile">의 HTML name 값은 바인딩된 C# 매개 변수/속성(FormFile)과 일치해야 합니다.For example, the HTML name value in <input type="file" name="formFile"> must match the C# parameter/property bound (FormFile). 자세한 내용은 이름 특성 값을 POST 메서드의 매개 변수 이름과 일치 섹션을 참조하세요.For more information, see the Match name attribute value to parameter name of POST method section.

다음 예제가 하는 일:The following example:

  • 하나 이상의 업로드된 파일을 반복합니다.Loops through one or more uploaded files.
  • Path.GetTempFileName을 사용하여 파일 이름을 포함하는 파일 전체 경로를 반환합니다.Uses Path.GetTempFileName to return a full path for a file, including the file name.
  • 앱에서 생성한 파일 이름을 사용하여 로컬 파일 시스템에 파일을 저장합니다.Saves the files to the local file system using a file name generated by the app.
  • 업로드된 파일의 총 수와 크기를 반환합니다.Returns the total number and size of files uploaded.
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 });
}

Path.GetRandomFileName을 사용하여 경로 없이 파일 이름을 생성합니다.Use Path.GetRandomFileName to generate a file name without a path. 다음 예제에서는 경로를 구성에서 가져옵니다.In the following example, the path is obtained from configuration:

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

에 전달된 경로는 FileStream 반드시 파일 이름을 포함해야 합니다.The path passed to the FileStream must include the file name. 파일 이름을 제공하지 않으면 런타임에 UnauthorizedAccessException이 발생합니다.If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.

IFormFile 기술을 사용하여 업로드된 파일은 처리 전에 서버의 메모리나 디스크에 버퍼링됩니다.Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. 작업 메서드 내부에서 IFormFile 내용을 Stream으로 액세스할 수 있습니다.Inside the action method, the IFormFile contents are accessible as a Stream. 로컬 파일 시스템 외에도 파일을 네트워크 공유 또는 파일 스토리지 서비스(예: Azure Blob Storage)에 저장할 수 있습니다.In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.

업로드를 위해 여러 파일에 대해 루프를 실행하고 안전한 파일 이름을 사용하는 또 하나의 예제는 샘플 앱에서 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 를 참조하세요.For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

경고

이전 임시 파일을 삭제하지 않고 65,535개를 초과하는 파일을 만들면 Path.GetTempFileNameIOException을 throw합니다.Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. 65535개 파일 제한은 서버당 제한입니다.The limit of 65,535 files is a per-server limit. Windows OS에서 이 제한에 대한 자세한 내용은 다음 항목의 설명을 참조하세요.For more information on this limit on Windows OS, see the remarks in the following topics:

버퍼링된 모델 바인딩을 사용하여 소용량 파일을 데이터베이스에 업로드Upload small files with buffered model binding to a database

Entity Framework를 사용하여 데이터베이스에 이진 파일 데이터를 저장하려면 엔터티에서 Byte 배열 속성을 정의합니다.To store binary file data in a database using Entity Framework, define a Byte array property on the entity:

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

IFormFile을 포함하는 클래스에 대한 페이지 모델 속성을 지정합니다.Specify a page model property for the class that includes an IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

참고

IFormFile은 작업 메서드 매개 변수 또는 바운딩 모델 속성으로 직접 사용할 수 있습니다.IFormFile can be used directly as an action method parameter or as a bound model property. 위 예제에서는 바인딩된 모델 속성을 사용합니다.The prior example uses a bound model property.

FileUpload Pages 폼에 사용 됩니다 Razor .The FileUpload is used in the Razor Pages form:

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

양식이 서버에 POST되면 IFormFile을 스트림으로 복사하여 데이터베이스에 바이트 배열로 저장합니다.When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. 다음 예제에서 _dbContext는 앱의 데이터베이스 컨텍스트를 저장합니다.In the following example, _dbContext stores the app's database context:

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

위의 예제는 샘플 앱에서 보여 주는 시나리오와 비슷합니다.The preceding example is similar to a scenario demonstrated in the sample app:

  • Pages/BufferedSingleFileUploadDb.cshtmlPages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.csPages/BufferedSingleFileUploadDb.cshtml.cs

경고

관계형 데이터베이스에 이진 데이터를 저장할 경우 성능에 나쁜 영향을 줄 수 있으므로 주의하세요.Use caution when storing binary data in relational databases, as it can adversely impact performance.

유효성 검사 없이 IFormFileFileName 속성을 의존하거나 신뢰하지 마세요.Don't rely on or trust the FileName property of IFormFile without validation. FileName 속성은 반드시 HTML 인코딩 후 표시 목적으로만 사용해야 합니다.The FileName property should only be used for display purposes and only after HTML encoding.

제시한 예제에서는 보안 고려 사항을 감안하지 않습니다.The examples provided don't take into account security considerations. 추가 정보는 다음 섹션과 샘플 앱에서 제공합니다.Additional information is provided by the following sections and the sample app:

스트리밍을 사용하여 대용량 파일 업로드Upload large files with streaming

다음 예제에서는 JavaScript를 사용하여 컨트롤러 작업에 파일을 스트리밍하는 방법을 보여 줍니다.The following example demonstrates how to use JavaScript to stream a file to a controller action. 사용자 지정 필터 특성을 사용하여 파일의 위조 방지 토큰이 생성되고 요청 본문 대신 클라이언트 HTTP 헤더에 전달됩니다.The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. 작업 메서드에서 업로드된 데이터를 직접 처리하므로 다른 사용자 지정 필터에서 형식 모델 바인딩을 사용할 수 없습니다.Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. 작업 내에서 양식의 콘텐츠는 각 개별 MultipartSection을 읽고 적절하게 파일을 처리하거나 콘텐츠를 저장하는 MultipartReader를 사용하여 읽습니다.Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 다중 파트 섹션을 읽은 후 작업에서 자체 모델 바인딩을 수행합니다.After the multipart sections are read, the action performs its own model binding.

초기 페이지 응답은 양식을 로드 하 고 특성을 통해의 위조 방지 토큰을에 저장 합니다 cookie GenerateAntiforgeryTokenCookieAttribute .The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute attribute). 특성은 ASP.NET Core의 기본 제공 위조 방지 지원 기능 을 사용 하 여 cookie 요청 토큰으로를 설정 합니다.The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:

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은 모델 바인딩을 사용하지 않도록 설정하는 데 사용됩니다.The DisableFormValueModelBindingAttribute is used to disable model binding:

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

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

샘플 앱에서 GenerateAntiforgeryTokenCookieAttribute 및는 페이지 DisableFormValueModelBindingAttribute /StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices Razor 규칙을 사용 하 여 및의 페이지 응용 프로그램 모델에 필터로 적용 됩니다.In the sample app, GenerateAntiforgeryTokenCookieAttribute and DisableFormValueModelBindingAttribute are applied as filters to the page application models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in Startup.ConfigureServices using Razor Pages conventions:

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

모델 바인딩은 양식을 읽지 않으므로 양식에서 바인딩된 매개 변수가 바인딩되지 않습니다(쿼리, 경로 및 헤더는 계속 작동함).Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). 작업 메서드는 Request 속성으로 직접 작동합니다.The action method works directly with the Request property. MultipartReader는 각 섹션을 읽는 데 사용됩니다.A MultipartReader is used to read each section. 키/값 데이터는 KeyValueAccumulator에 저장됩니다.Key/value data is stored in a KeyValueAccumulator. 다중 파트 섹션을 읽은 후 KeyValueAccumulator의 내용이 양식 데이터를 모델 형식으로 바인딩하는 데 사용됩니다.After the multipart sections are read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

EF Core를 사용하여 데이터베이스에 스트리밍하기 위한 전체 StreamingController.UploadDatabase 메서드는 다음과 같습니다.The complete StreamingController.UploadDatabase method for streaming to a database with 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 = new byte[0];

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

실제 위치로 스트리밍하기 위한 전체 StreamingController.UploadPhysical 메서드는 다음과 같습니다.The complete StreamingController.UploadPhysical method for streaming to a physical location:

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

샘플 앱에서 유효성 검사는 FileHelpers.ProcessStreamedFile에 의해 처리됩니다.In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile.

유효성 검사Validation

샘플 앱의 FileHelpers 클래스는 버퍼링된 IFormFile 및 스트리밍된 파일 업로드에 대한 여러 검사를 보여 줍니다.The sample app's FileHelpers class demonstrates a several checks for buffered IFormFile and streamed file uploads. 샘플 앱에서 IFormFile 버퍼링된 파일 업로드를 처리하려면 Utilities/FileHelpers.cs 파일에서 ProcessFormFile 메서드를 참조하세요.For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. 스트리밍된 파일의 처리는 동일한 파일의 ProcessStreamedFile 메서드를 참조하세요.For processing streamed files, see the ProcessStreamedFile method in the same file.

경고

샘플 앱에서 보여 주는 유효성 검사 처리 메서드는 업로드된 파일의 내용을 검사하지 않습니다.The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. 대부분의 프로덕션 시나리오에서는 사용자 또는 다른 시스템에서 파일을 사용할 수 있도록 하기 전에 파일에 바이러스/맬웨어 스캐너 API를 사용합니다.In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

항목 샘플에서는 유효성 검사 기술에 대한 작업 예제를 제공하지만, 다음과 같은 경우가 아니면 프로덕션 앱에서 FileHelpers 클래스를 구현하지 마세요.Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers class in a production app unless you:

  • 구현을 완전히 이해합니다.Fully understand the implementation.
  • 앱의 환경 및 사양에 맞게 구현을 수정합니다.Modify the implementation as appropriate for the app's environment and specifications.

이러한 요구 사항을 해결하지 않고 앱에서 보안 코드를 무분별하게 구현해서는 안 됩니다.Never indiscriminately implement security code in an app without addressing these requirements.

콘텐츠 유효성 검사Content validation

업로드된 콘텐츠에 타사 바이러스/맬웨어 검사 API를 사용합니다.Use a third party virus/malware scanning API on uploaded content.

대용량 시나리오에서 파일 검사는 많은 서버 리소스를 요구합니다.Scanning files is demanding on server resources in high volume scenarios. 파일 검사로 인해 요청 처리 성능이 저하된 경우 검사 작업을 백그라운드 서비스로 오프로드하는 것이 좋습니다.이 경우 앱 서버와 다른 서버에서 실행되는 서비스가 있을 수 있습니다.If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. 일반적으로 업로드된 파일은 백그라운드 바이러스 검사 프로그램에서 검사될 때까지 격리된 영역에 저장됩니다.Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. 파일이 전달되면 파일이 일반 파일 스토리지 위치로 이동됩니다.When a file passes, the file is moved to the normal file storage location. 이러한 단계는 일반적으로 파일의 검사 상태를 나타내는 데이터베이스 레코드와 함께 수행됩니다.These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. 이러한 방법을 사용하여 앱 및 앱 서버는 요청에 응답하는 데 집중합니다.By using such an approach, the app and app server remain focused on responding to requests.

파일 확장명 유효성 검사File extension validation

업로드된 파일의 확장명을 허용된 확장명 목록에 따라 확인해야 합니다.The uploaded file's extension should be checked against a list of permitted extensions. 예를 들면 다음과 같습니다.For example:

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
}

파일 서명 유효성 검사File signature validation

파일의 서명은 파일 시작 부분의 처음 몇 바이트에 의해 결정됩니다.A file's signature is determined by the first few bytes at the start of a file. 이러한 바이트는 확장명이 파일 내용과 일치하는지 여부를 나타내는 데 사용할 수 있습니다.These bytes can be used to indicate if the extension matches the content of the file. 샘플 앱은 몇 가지 일반적인 파일 형식에 대한 파일 서명을 확인합니다.The sample app checks file signatures for a few common file types. 다음 예제에서는 파일에서 JPEG 이미지 파일에 대한 서명을 확인합니다.In the following example, the file signature for a JPEG image is checked against the 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));
}

추가 파일 서명을 얻으려면 파일 서명 데이터베이스 및 공식 파일 사양을 참조하세요.To obtain additional file signatures, see the File Signatures Database and official file specifications.

파일 이름 보안File name security

실제 스토리지에 파일을 저장하는 데 클라이언트에서 제공하는 파일 이름을 사용하지 마세요.Never use a client-supplied file name for saving a file to physical storage. Path.GetRandomFileName 또는 Path.GetTempFileName을 사용하여 임시 스토리지에 대한 전체 경로(파일 이름을 포함)를 만들어 파일에 대한 안전한 파일 이름을 만듭니다.Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.

Razor 자동으로 표시 하기 위해 속성 값을 인코딩합니다.Razor automatically HTML encodes property values for display. 다음 코드는 안전하게 사용할 수 있습니다.The following code is safe to use:

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

외부 Razor 에서는 항상 HtmlEncode 사용자 요청의 파일 이름 콘텐츠를 사용 합니다.Outside of Razor, always HtmlEncode file name content from a user's request.

많은 구현에서 파일 존재 여부에 대한 확인이 포함되어야 합니다. 그렇지 않으면 파일이 같은 이름의 파일을 덮어씁니다.Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. 앱의 사양을 충족하는 추가 논리를 제공합니다.Supply additional logic to meet your app's specifications.

크기 유효성 검사Size validation

업로드된 파일의 크기를 제한합니다.Limit the size of uploaded files.

샘플 앱에서 파일 크기는 2MB(바이트 단위로 표시)로 제한됩니다.In the sample app, the size of the file is limited to 2 MB (indicated in bytes). 이 제한은 파일의 구성을 통해 제공 됩니다 appsettings.json .The limit is supplied via Configuration from the appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel 클래스에 삽입됩니다.The FileSizeLimit is injected into PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

파일 크기가 제한을 초과하는 파일은 거부됩니다.When a file size exceeds the limit, the file is rejected:

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

이름 특성 값을 POST 메서드의 매개 변수 이름과 일치Match name attribute value to parameter name of POST method

폼 데이터를 Razor 게시 하거나 JavaScript를 직접 사용 하는 형식이 아닌 FormData 경우 폼의 요소에 지정 된 이름이 나 FormData 컨트롤러 작업의 매개 변수 이름과 일치 해야 합니다.In non-Razor forms that POST form data or use JavaScript's FormData directly, the name specified in the form's element or FormData must match the name of the parameter in the controller's action.

다음 예제에서는In the following example:

  • <input> 요소를 사용하는 경우 name 특성은 값 battlePlans로 설정됩니다.When using an <input> element, the name attribute is set to the value battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript에서 FormData를 사용하는 경우 이름이 값 battlePlans로 설정됩니다.When using FormData in JavaScript, the name is set to the value battlePlans:

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

C# 메서드(battlePlans)의 매개 변수와 일치하는 이름을 사용합니다.Use a matching name for the parameter of the C# method (battlePlans):

  • Razor페이지 페이지 처리기 메서드를 Upload 다음과 같이 지정 합니다.For a Razor Pages page handler method named Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST 컨트롤러 작업 메서드:For an MVC POST controller action method:

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

서버 및 앱 구성Server and app configuration

다중 파트 본문 길이 제한Multipart body length limit

MultipartBodyLengthLimit에서 각 다중 파트 본문의 길이에 대한 제한을 설정합니다.MultipartBodyLengthLimit sets the limit for the length of each multipart body. 양식 섹션이 이 제한을 초과하면 구문 분석할 때 InvalidDataException이 throw됩니다.Form sections that exceed this limit throw an InvalidDataException when parsed. 기본값은 134,217,728(128MB)입니다.The default is 134,217,728 (128 MB). Startup.ConfigureServices에서 MultipartBodyLengthLimit 설정을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices:

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

RequestFormLimitsAttribute는 단일 페이지 또는 작업에 대해 MultipartBodyLengthLimit을 설정하는 데 사용됩니다.RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in 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 앱 또는 MVC 앱에서 필터를 페이지 모델 또는 작업 메서드에 적용 합니다.In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

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

Kestrel 최대 요청 본문 크기Kestrel maximum request body size

Kestrel에서 호스트되는 앱의 경우 기본 최대 요청 본문 크기는 30,000,000바이트(약 28.6MB)입니다.For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. MaxRequestBodySize Kestrel 서버 옵션을 사용하여 제한을 사용자 지정합니다.Customize the limit using the MaxRequestBodySize Kestrel server option:

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는 단일 페이지 또는 작업에 대해 MaxRequestBodySize를 설정하는 데 사용됩니다.RequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or action.

Razor페이지 앱에서 다음과 같은 규칙 을 사용 하 여 필터를 적용 합니다 Startup.ConfigureServices .In a Razor Pages app, apply the filter with a convention in 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 앱 또는 MVC 앱에서 필터를 페이지 처리기 클래스 또는 작업 메서드에 적용 합니다.In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

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

기타 Kestrel 제한Other Kestrel limits

다른 Kestrel 제한이 Kestrel에서 호스트되는 앱에 적용될 수 있습니다.Other Kestrel limits may apply for apps hosted by Kestrel:

IISIIS

기본 요청 제한 ( maxAllowedContentLength )은 3000만 바이트 이며 약 28.6 MB입니다.The default request limit (maxAllowedContentLength) is 30,000,000 bytes, which is approximately 28.6 MB. 파일의 제한을 사용자 지정 합니다 web.config .Customize the limit in the web.config file. 다음 예제에서 제한은 50 (52428800 바이트)로 설정 됩니다.In the following example, the limit is set to 50 MB (52,428,800 bytes):

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

maxAllowedContentLength설정은 IIS에만 적용 됩니다.The maxAllowedContentLength setting only applies to IIS. 자세한 내용은 요청 제한 <requestLimits> 을 참조 하세요.For more information, see Request Limits <requestLimits>.

에서을 설정 하 여 HTTP 요청에 대 한 최대 요청 본문 크기를 늘립니다 IISServerOptions.MaxRequestBodySize Startup.ConfigureServices .Increase the maximum request body size for the HTTP request by setting IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. 다음 예제에서 제한은 50 (52428800 바이트)로 설정 됩니다.In the following example, the limit is set to 50 MB (52,428,800 bytes):

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

자세한 내용은 IIS가 있는 Windows에서 ASP.NET Core 호스팅를 참조하세요.For more information, see IIS가 있는 Windows에서 ASP.NET Core 호스팅.

문제 해결Troubleshoot

다음은 파일 업로드 및 가능한 솔루션을 사용하여 작업할 때 자주 발생하는 몇 가지 일반적인 문제입니다.Below are some common problems encountered when working with uploading files and their possible solutions.

IIS 서버에 배포할 때 찾을 수 없음 오류Not Found error when deployed to an IIS server

다음 오류는 업로드된 파일이 서버의 구성된 콘텐츠 길이를 초과했음을 나타냅니다.The following error indicates that the uploaded file exceeds the server's configured content length:

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

자세한 내용은 IIS 섹션을 참조하세요.For more information, see the IIS section.

연결 실패Connection failure

연결 오류 및 서버 연결 다시 설정은 업로드된 파일이 Kestrel의 최대 요청 본문 크기를 초과함을 나타낼 수 있습니다.A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. 자세한 내용은 Kestrel 최대 요청 본문 크기 섹션을 참조하세요.For more information, see the Kestrel maximum request body size section. Kestrel 클라이언트 연결 제한을 조정해야 할 수도 있습니다.Kestrel client connection limits may also require adjustment.

IFormFile 사용 시 Null 참조 예외Null Reference Exception with IFormFile

컨트롤러에서 IFormFile을 사용하여 업로드된 파일을 수락하지만 값이 null이면 HTML 양식에서 multipart/form-dataenctype 값을 지정하는지 확인합니다.If the controller is accepting uploaded files using IFormFile but the value is null, confirm that the HTML form is specifying an enctype value of multipart/form-data. <form> 요소에서 이 특성이 설정되지 않으면 파일 업로드가 실행되지 않고 바인딩된 IFormFile 인수는 모두 null이 됩니다.If this attribute isn't set on the <form> element, the file upload doesn't occur and any bound IFormFile arguments are null. 또한 양식 데이터의 업로드 이름 지정이 앱의 이름 지정과 일치하는지 확인합니다.Also confirm that the upload naming in form data matches the app's naming.

스트림이 너무 깁니다.Stream was too long

이 항목의 예제에서는 업로드된 파일의 콘텐츠를 저장하는 데 MemoryStream에 의존합니다.The examples in this topic rely upon MemoryStream to hold the uploaded file's content. MemoryStream의 크기 제한은 int.MaxValue입니다.The size limit of a MemoryStream is int.MaxValue. 앱의 파일 업로드 시나리오에서 50MB보다 큰 파일 콘텐츠를 보관해야 하는 경우 업로드된 파일의 콘텐츠를 보관할 때 단일 MemoryStream에 의존하지 않는 대체 방법을 사용하세요.If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream for holding an uploaded file's content.

추가 리소스Additional resources