diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index 35e820337b86..2721907d905b 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -236,6 +236,37 @@ You can address this by flowing prerendered state using the Persistent Component :::moniker-end +:::moniker range=">= aspnetcore-9.0" + +## Client-side request streaming + +For Chromium-based browsers (for example, Google Chrome and Microsoft Edge) using the HTTP/2 protocol, HTTPS, and [CORS](xref:security/cors), client-side Blazor uses [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API) to permit [request streaming](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests). + +To enable request streaming, set to `true` on the . + +In the following file upload example: + +* `content` is the file's . +* `/Filesave` is the web API endpoint. +* `Http` is the . + +```csharp +var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave"); +request.SetBrowserRequestStreamingEnabled(true); +request.Content = content; + +var response = await Http.SendAsync(request); +``` + +Streaming requests: + +* Require HTTPS protocol and don't work on HTTP/1.x. +* Include a body but not a `Content-Length` header. [CORS](xref:security/cors) is required, and a CORS preflight request is always issued. + +For more information on file uploads with an component, see and the example at [Upload files to a server with client-side rendering (CSR)](xref:blazor/file-uploads#upload-files-to-a-server-with-client-side-rendering-csr). + +:::moniker-end + ## Add the `HttpClient` service *The guidance in this section applies to client-side scenarios.* @@ -251,10 +282,7 @@ In the `Program` file, add an service if it is ```csharp builder.Services.AddScoped(sp => - new HttpClient - { - BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) - }); + new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); ``` The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. @@ -268,10 +296,7 @@ If you're calling an external web API (not in the same URL space as the client a ```csharp builder.Services.AddScoped(sp => - new HttpClient - { - BaseAddress = new Uri("https://localhost:5001") - }); + new HttpClient { BaseAddress = new Uri("https://localhost:5001") }); ``` ## JSON helpers diff --git a/aspnetcore/blazor/file-uploads.md b/aspnetcore/blazor/file-uploads.md index bd021fb4e90b..81b57b69d513 100644 --- a/aspnetcore/blazor/file-uploads.md +++ b/aspnetcore/blazor/file-uploads.md @@ -101,26 +101,88 @@ If you're using the [Autofac Inversion of Control (IoC) container](https://autof ## File size read and upload limits -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-9.0" + +Server-side or client-side, there's no file read or upload size limit for the component. + +For Chromium-based browsers (for example, Google Chrome and Microsoft Edge) using the HTTP/2 protocol, HTTPS, and [CORS](xref:security/cors), client-side Blazor uses [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API) to permit uploading large files (over 250 MB or larger than the device's available memory) with [request streaming](xref:blazor/call-web-api#client-side-request-streaming). + +For non-Chromium browsers or without HTTP/2 protocol/HTTPS/CORS, client-side Blazor reads the file's bytes into a single JavaScript array buffer when marshalling the data from JavaScript to C#, which is limited to 2 GB or to the device's available memory. Large file uploads, typically 250 MB or larger, may fail for client-side uploads using the component. For more information, see the following discussions: + +* [The Blazor InputFile Component should handle chunking when the file is uploaded (dotnet/runtime #84685)](https://github.com/dotnet/runtime/issues/84685) +* [Request Streaming upload via http handler (dotnet/runtime #36634)](https://github.com/dotnet/runtime/issues/36634) + +For large client-side file uploads that don't meet the preceding criteria for request streaming and fail when attempting to use the component, we recommend chunking large files with a custom component using multiple [HTTP range requests](https://developer.mozilla.org/docs/Web/HTTP/Range_requests) instead of using the component. + +:::moniker-end + +:::moniker range=">= aspnetcore-6.0 < aspnetcore-9.0" Server-side or client-side, there's no file read or upload size limit specifically for the component. However, client-side Blazor reads the file's bytes into a single JavaScript array buffer when marshalling the data from JavaScript to C#, which is limited to 2 GB or to the device's available memory. Large file uploads (> 250 MB) may fail for client-side uploads using the component. For more information, see the following discussions: +* [The Blazor InputFile Component should handle chunking when the file is uploaded (dotnet/runtime #84685)](https://github.com/dotnet/runtime/issues/84685) +* [Request Streaming upload via http handler (dotnet/runtime #36634)](https://github.com/dotnet/runtime/issues/36634) + +For large client-side file uploads that fail when attempting to use the component, we recommend: + +* Adopting .NET 9 or later for Chromium-based browsers (Google Chrome and Microsoft Edge) with HTTP/2 protocol support, where this problem is internally addressed by Blazor with [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API). +* For non-Chromium browsers, Chromium browsers without HTTP/2, or .NET 8 or earlier, chunking large files with a custom component using multiple [HTTP range requests](https://developer.mozilla.org/docs/Web/HTTP/Range_requests) instead of using the component. + :::moniker-end :::moniker range="< aspnetcore-6.0" The maximum supported file size for the component is 2 GB. Additionally, client-side Blazor reads the file's bytes into a single JavaScript array buffer when marshalling the data from JavaScript to C#, which is limited to 2 GB or to the device's available memory. Large file uploads (> 250 MB) may fail for client-side uploads using the component. For more information, see the following discussions: -:::moniker-end - * [The Blazor InputFile Component should handle chunking when the file is uploaded (dotnet/runtime #84685)](https://github.com/dotnet/runtime/issues/84685) * [Request Streaming upload via http handler (dotnet/runtime #36634)](https://github.com/dotnet/runtime/issues/36634) -For large client-side file uploads that fail when attempting to use the component, we recommend chunking large files with a custom component using multiple [HTTP range requests](https://developer.mozilla.org/docs/Web/HTTP/Range_requests) instead of using the component. +* Adopting .NET 9 or later for Chromium-based browsers (Google Chrome and Microsoft Edge) with HTTP/2 protocol support, where this problem is internally addressed by Blazor with [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API). +* For non-Chromium browsers, Chromium browsers without HTTP/2, or .NET 5 or earlier, chunking large files with a custom component using multiple [HTTP range requests](https://developer.mozilla.org/docs/Web/HTTP/Range_requests) instead of using the component. + +:::moniker-end + +## Security considerations + +### Avoid `IBrowserFile.Size` for file size limits + +Avoid using to impose a limit on the file size. + + The following approach is ***insecure*** and must be avoided: - +```diff +- var fileContent = new StreamContent(file.OpenReadStream(file.Size)); +``` + +Instead of using the unsafe client-supplied file size, explicitly specify the maximum file size. The following example sets the maximum file size (`maxFileSize`) to 15 K: + +```csharp +long maxFileSize = 1024 * 15; -Work is currently scheduled for .NET 9 (late 2024) to address the client-side file size upload limitation. +... + +var fileContent = new StreamContent(file.OpenReadStream(maxFileSize)); +``` + +### File name security + +Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using or to create a full path (including the file name) for temporary storage. + +Razor automatically HTML encodes property values for display. The following code is safe to use: + +```cshtml +@foreach (var file in Model.DatabaseFiles) { + + + @file.UntrustedName + + +} +``` + +Outside of Razor, always use to safely encode file names 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. ## Examples @@ -336,7 +398,7 @@ The following `FileUpload2` component: :::moniker range=">= aspnetcore-8.0" -If the component limits file uploads to a single file at a time or if the component only adopts interactive client-side rendering (CSR, `InteractiveWebAssembly`), the component can avoid the use of the `LazyBrowserFileStream` and use a . The following demonstrates the changes for the `FileUpload2` component: +If the component limits file uploads to a single file at a time or if the component only adopts client-side rendering (CSR, `InteractiveWebAssembly`), the component can avoid the use of the `LazyBrowserFileStream` and use a . The following demonstrates the changes for the `FileUpload2` component: ```diff - var stream = new LazyBrowserFileStream(file, maxFileSize); @@ -480,6 +542,12 @@ The server app must register controller services and map controller endpoints. F The following example demonstrates uploading files to a backend web API controller in a separate app, possibly on a separate server, from a component in a Blazor Web App that adopts CSR or a component in a Blazor WebAssembly app. +:::moniker range=">= aspnetcore-9.0" + +The example adopts [request streaming](xref:blazor/call-web-api#client-side-request-streaming) for a Chromium-based browser (for example, Google Chrome or Microsoft Edge) with HTTP/2 protocol, [CORS](xref:security/cors), and HTTPS. If request streaming can't be used, Blazor gracefully degrades to [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) without request streaming. For more information, see the [File size read and upload limits](#file-size-read-and-upload-limits) section. + +:::moniker-end + The following `UploadResult` class maintains the result of an uploaded file. When a file fails to upload on the server, an error code is returned in `ErrorCode` for display to the user. A safe file name is generated on the server for each file and returned to the client in `StoredFileName` for display. Files are keyed between the client and server using the unsafe/untrusted file name in `FileName`. `UploadResult.cs`: @@ -518,15 +586,15 @@ A security best practice for production apps is to avoid sending error messages :::moniker range=">= aspnetcore-8.0" -In the Blazor Web App main project, add and related services in the project's `Program` file: +In the Blazor Web App server project, add and related services in the project's `Program` file: ```csharp builder.Services.AddHttpClient(); ``` -The `HttpClient` services must be added to the main project because the client-side component is prerendered on the server. If you [disable prerendering for the following component](xref:blazor/components/render-modes#prerendering), you aren't required to provide the `HttpClient` services in the main app and don't need to add the preceding line to the main project. +The services must be added to the server project because the client-side component is prerendered on the server. If you [disable prerendering for the following component](xref:blazor/components/render-modes#prerendering), you aren't required to provide the services in the server project and don't need to add the preceding line to the server project. -For more information on adding `HttpClient` services to an ASP.NET Core app, see . +For more information on adding services to an ASP.NET Core app, see . The client project (`.Client`) of a Blazor Web App must also register an for HTTP POST requests to a backend web API controller. Confirm or add the following to the client project's `Program` file: @@ -537,17 +605,177 @@ builder.Services.AddScoped(sp => The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. If you're calling an external web API, set the URI to the web API's base address. -Specify the Interactive WebAssembly render mode attribute at the top of the following component in a Blazor Web App: +A standalone Blazor WebAssembly app that uploads files to a separate server web API either uses a [named `HttpClient`](xref:blazor/call-web-api#named-httpclient-with-ihttpclientfactory) or sets the default service registration to point to the web API's endpoint. In the following example where the web API is hosted locally at port 5001, the base address is `https://localhost:5001`: + +```csharp +builder.Services.AddScoped(sp => + new HttpClient { BaseAddress = new Uri("https://localhost:5001") }); +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-9.0" + +In a Blazor Web App, add the namespace to the component's directives: ```razor -@rendermode InteractiveWebAssembly +@using Microsoft.AspNetCore.Components.WebAssembly.Http ``` :::moniker-end `FileUpload2.razor`: -:::moniker range=">= aspnetcore-8.0" +:::moniker range=">= aspnetcore-9.0" + +```razor +@page "/file-upload-2" +@using System.Linq +@using System.Net.Http.Headers +@using System.Net +@inject HttpClient Http +@inject ILogger Logger + +File Upload 2 + +

File Upload Example 2

+ +

+ +

+ +@if (files.Count > 0) +{ +
+
+
    + @foreach (var file in files) + { +
  • + File: @file.Name +
    + @if (FileUpload(uploadResults, file.Name, Logger, + out var result)) + { + + Stored File Name: @result.StoredFileName + + } + else + { + + There was an error uploading the file + (Error: @result.ErrorCode). + + } +
  • + } +
+
+
+} + +@code { + private List files = new(); + private List uploadResults = new(); + private int maxAllowedFiles = 3; + private bool shouldRender; + + protected override bool ShouldRender() => shouldRender; + + private async Task OnInputFileChange(InputFileChangeEventArgs e) + { + shouldRender = false; + long maxFileSize = 1024 * 15; + var upload = false; + + using var content = new MultipartFormDataContent(); + + foreach (var file in e.GetMultipleFiles(maxAllowedFiles)) + { + if (uploadResults.SingleOrDefault( + f => f.FileName == file.Name) is null) + { + try + { + files.Add(new() { Name = file.Name }); + + var fileContent = new StreamContent(file.OpenReadStream(maxFileSize)); + + fileContent.Headers.ContentType = + new MediaTypeHeaderValue(file.ContentType); + + content.Add( + content: fileContent, + name: "\"files\"", + fileName: file.Name); + + upload = true; + } + catch (Exception ex) + { + Logger.LogInformation( + "{FileName} not uploaded (Err: 6): {Message}", + file.Name, ex.Message); + + uploadResults.Add( + new() + { + FileName = file.Name, + ErrorCode = 6, + Uploaded = false + }); + } + } + } + + if (upload) + { + var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave"); + request.SetBrowserRequestStreamingEnabled(true); + request.Content = content; + + var response = await Http.SendAsync(request); + + var newUploadResults = await response.Content + .ReadFromJsonAsync>(); + + if (newUploadResults is not null) + { + uploadResults = uploadResults.Concat(newUploadResults).ToList(); + } + } + + shouldRender = true; + } + + private static bool FileUpload(IList uploadResults, + string? fileName, ILogger logger, out UploadResult result) + { + result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new(); + + if (!result.Uploaded) + { + logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName); + result.ErrorCode = 5; + } + + return result.Uploaded; + } + + private class File + { + public string? Name { get; set; } + } +} +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" :::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_WebAssembly/Pages/FileUpload2.razor"::: @@ -587,7 +815,13 @@ Because the example uses the app's [environment](xref:blazor/fundamentals/enviro > [!WARNING] > The example saves files without scanning their contents, and the guidance in this article doesn't take into account additional security best practices for uploaded files. On staging and production systems, disable execute permission on the upload folder and scan files with an anti-virus/anti-malware scanner API immediately after upload. For more information, see . -In the following example, update the shared project's namespace to match the shared project if a shared project is supplying the `UploadResult` class. +In the following example for a hosted Blazor WebAssembly app or where a shared project is used to supply the `UploadResult` class, add the shared project's namespace: + +```csharp +using BlazorSample.Shared; +``` + +We recommend using a namespace for the following controller (for example: `namespace BlazorSample.Controllers`). `Controllers/FilesaveController.cs`: @@ -600,7 +834,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using BlazorSample.Shared; [ApiController] [Route("[controller]")] @@ -687,15 +920,72 @@ public class FilesaveController( In the preceding code, is called to generate a secure file name. Never trust the file name provided by the browser, as a cyberattacker may choose an existing file name that overwrites an existing file or send a path that attempts to write outside of the app. -The server app must register controller services and map controller endpoints. For more information, see . +The server app must register controller services and map controller endpoints. For more information, see . We recommend adding controller services with in order to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery) for authenticated users. If you merely use , antiforgery isn't enabled automatically. For more information, see . + +:::moniker range=">= aspnetcore-9.0" + +Cross-Origin Requests (CORS) configuration on the server is required for [request streaming](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests), and a preflight request is always made by the client. In the service configuration of the server's `Program` file (the server project of a Blazor Web App or the backend server web API of a Blazor WebAssembly app), the following default CORS policy is suitable for testing with the examples in this article. The client makes the local request from port 5003. Change the port number to match the client app port that you're using: - +```csharp +app.UseCors(); +``` + +For more information, see . + +:::moniker range=">= aspnetcore-9.0" + +Configure the server's maximum request body size and multipart body length limits if the limits constrain the upload size. + +For the Kestrel server, set (default: 30,000,000 bytes) and (default: 134,217,728 bytes). Set the `maxFileSize` variable in the component and the controller to the same value. + +In the following `Program` file Kestrel configuration (the server project of a Blazor Web App or the backend server web API of a Blazor WebAssembly app), the `{LIMIT}` placeholder is the limit in bytes: + +```csharp +using Microsoft.AspNetCore.Http.Features; + +... + +builder.WebHost.ConfigureKestrel(serverOptions => +{ + serverOptions.Limits.MaxRequestBodySize = {LIMIT}; +}); + +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = {LIMIT}; +}); +``` + +:::moniker-end ## Cancel a file upload diff --git a/aspnetcore/release-notes/aspnetcore-9/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-9/includes/blazor.md index 8c5a71644ec0..ccbd5a57af7b 100644 --- a/aspnetcore/release-notes/aspnetcore-9/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-9/includes/blazor.md @@ -227,3 +227,12 @@ Trigger JavaScript callbacks either before or after enhanced navigation with new * `blazor.addEventListener("enhancednavigationend", {CALLBACK})` For more information, see . + +### Client-side request streaming + +For Chromium-based browsers (for example, Google Chrome and Microsoft Edge) using the HTTP/2 protocol, [CORS](xref:security/cors), and HTTPS, client-side Blazor uses [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API) to permit [request streaming](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests). + +For more information, see the following resources: + +* +*