From f538c9ad7fdad18233b8e34d8ab3030cbd647357 Mon Sep 17 00:00:00 2001 From: Robert Haken Date: Thu, 12 Dec 2024 11:05:35 +0100 Subject: [PATCH 1/8] [Blazor] Security - interactive-server-side-rendering - script tags (#34202) --- .../security/interactive-server-side-rendering.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aspnetcore/blazor/security/interactive-server-side-rendering.md b/aspnetcore/blazor/security/interactive-server-side-rendering.md index 6107d84c4723..22617f8290ef 100644 --- a/aspnetcore/blazor/security/interactive-server-side-rendering.md +++ b/aspnetcore/blazor/security/interactive-server-side-rendering.md @@ -391,10 +391,21 @@ In addition to the safeguards that the framework implements, the app must be cod For a XSS vulnerability to exist, the app must incorporate user input in the rendered page. Blazor executes a compile-time step where the markup in a `.razor` file is transformed into procedural C# logic. At runtime, the C# logic builds a *render tree* describing the elements, text, and child components. This is applied to the browser's DOM via a sequence of JavaScript instructions (or is serialized to HTML in the case of prerendering): +:::moniker range=">= aspnetcore-8.0" + +* User input rendered via normal Razor syntax (for example, `@someStringValue`) doesn't expose a XSS vulnerability because the Razor syntax is added to the DOM via commands that can only write text. Even if the value includes HTML markup, the value is displayed as static text. When prerendering, the output is HTML-encoded, which also displays the content as static text. +* Component authors can author components in C# without using Razor. The component author is responsible for using the correct APIs when emitting output. For example, use `builder.AddContent(0, someUserSuppliedString)` and *not* `builder.AddMarkupContent(0, someUserSuppliedString)`, as the latter could create a XSS vulnerability. + +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + * User input rendered via normal Razor syntax (for example, `@someStringValue`) doesn't expose a XSS vulnerability because the Razor syntax is added to the DOM via commands that can only write text. Even if the value includes HTML markup, the value is displayed as static text. When prerendering, the output is HTML-encoded, which also displays the content as static text. * Script tags aren't allowed and shouldn't be included in the app's component render tree. If a script tag is included in a component's markup, a compile-time error is generated. * Component authors can author components in C# without using Razor. The component author is responsible for using the correct APIs when emitting output. For example, use `builder.AddContent(0, someUserSuppliedString)` and *not* `builder.AddMarkupContent(0, someUserSuppliedString)`, as the latter could create a XSS vulnerability. +:::moniker-end + Consider further mitigating XSS vulnerabilities. For example, implement a restrictive [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/CSP). For more information, see . For more information, see . From 9214ff58598e89dbae488c64a8dd003677c66470 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 12 Dec 2024 05:24:31 -0500 Subject: [PATCH 2/8] Update script tag location guidance (#34346) --- .../location-of-javascript.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/aspnetcore/blazor/javascript-interoperability/location-of-javascript.md b/aspnetcore/blazor/javascript-interoperability/location-of-javascript.md index 74f95d165e0f..cee1ec797a16 100644 --- a/aspnetcore/blazor/javascript-interoperability/location-of-javascript.md +++ b/aspnetcore/blazor/javascript-interoperability/location-of-javascript.md @@ -33,17 +33,17 @@ Load JavaScript (JS) code using any of the following approaches: :::moniker-end +## Location of ``) inside the [closing `` element](xref:blazor/project-structure#location-of-head-and-body-content) after the Blazor script reference: @@ -105,7 +105,7 @@ Place the JavaScript tags (``) inside the [closing `` :::moniker range=">= aspnetcore-6.0" -### Load a script from an external JavaScript file (`.js`) collocated with a component +## Load a script from an external JavaScript file (`.js`) collocated with a component [!INCLUDE[](~/blazor/includes/js-interop/js-collocation.md)] @@ -170,7 +170,7 @@ For more information, see . :::moniker range=">= aspnetcore-6.0" -### Inject a script before or after Blazor starts +## Inject a script before or after Blazor starts To ensure scripts load before or after Blazor starts, use a JavaScript initializer. For more information and examples, see . @@ -178,7 +178,7 @@ To ensure scripts load before or after Blazor starts, use a JavaScript initializ :::moniker range="< aspnetcore-6.0" -### Inject a script after Blazor starts +## Inject a script after Blazor starts To inject a script after Blazor starts, chain to the `Promise` that results from a manual start of Blazor. For more information and an example, see . From 08a87db36f755645847d3ed809fce78e817a956d Mon Sep 17 00:00:00 2001 From: Robert Haken Date: Thu, 12 Dec 2024 11:27:29 +0100 Subject: [PATCH 3/8] [Blazor] State management - StateHasChanged+InvokeAsync (#34237) --- aspnetcore/blazor/state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/state-management.md b/aspnetcore/blazor/state-management.md index 2a80d762bed7..c7734f37e2b0 100644 --- a/aspnetcore/blazor/state-management.md +++ b/aspnetcore/blazor/state-management.md @@ -791,7 +791,7 @@ When implementing custom state storage, a useful approach is to adopt [cascading ## Troubleshoot -In a custom state management service, a callback invoked outside of Blazor's synchronization context must wrap the logic of the callback in to move it onto the renderer's synchronization context. +When using a custom state management service where you want to support state modifications from outside Blazor's synchronization context (for example from a timer or a background service), all consuming components must wrap the call in . This ensures the change notification is handled on the renderer's synchronization context. When the state management service doesn't call on Blazor's synchronization context, the following error is thrown: From a19f35a4af26cff1034307d071c886aab06c34b7 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 12 Dec 2024 05:45:20 -0500 Subject: [PATCH 4/8] Clarify the goal of EF Core code-first (#34347) --- aspnetcore/blazor/tutorials/movie-database-app/part-2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/tutorials/movie-database-app/part-2.md b/aspnetcore/blazor/tutorials/movie-database-app/part-2.md index 38cffb270f7d..c5f4fecd887b 100644 --- a/aspnetcore/blazor/tutorials/movie-database-app/part-2.md +++ b/aspnetcore/blazor/tutorials/movie-database-app/part-2.md @@ -319,7 +319,7 @@ EF Core adopts the *code-first* approach for database design and maintenance: * Entity classes are created and updated first in the app. * The database is created and updated from the app's entity classes. -This is the reverse procedure of *database-first* approaches, where the database is designed, built, and updated first. Adopting EF Core's code-first approach speeds up the process of app development because most of the difficult and time-consuming database creation and management procedures are handled transparently by the EF Core tooling, so you can focus on app development. +This is the reverse procedure of *database-first* approaches, where the database is designed, built, and updated first. Adopting EF Core's code-first approach aims to speed up the process of app development because most of the difficult and time-consuming database creation and management procedures are handled transparently by the EF Core tooling, so you can focus on app development. :::zone pivot="vs" From 9bac552349e83bc3374d3f90d7277c2882b7417b Mon Sep 17 00:00:00 2001 From: Robert Haken Date: Thu, 12 Dec 2024 11:48:48 +0100 Subject: [PATCH 5/8] [Blazor] Performance - Parameter values bundling vs. change detection (#34291) --- aspnetcore/blazor/performance.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/performance.md b/aspnetcore/blazor/performance.md index 41a4c4229e06..953c8b57f8d5 100644 --- a/aspnetcore/blazor/performance.md +++ b/aspnetcore/blazor/performance.md @@ -228,7 +228,9 @@ To reduce parameter load, bundle multiple parameters in a custom class. For exam } ``` -However, consider that it might be an improvement not to have a table cell component, as shown in the preceding example, and instead [inline its logic into the parent component](#inline-child-components-into-their-parents). +However, keep in mind that bundling primitive parameters into a class isn't always an advantage. While it can reduce parameter count, it also impacts how change detection and rendering behave. Passing non-primitive parameters always triggers a re-render, because Blazor can't know whether arbitrary objects have internally mutable state, whereas passing primitive parameters only triggers a re-render if their values have actually changed. + +Also, consider that it might be an improvement not to have a table cell component, as shown in the preceding example, and instead [inline its logic into the parent component](#inline-child-components-into-their-parents). > [!NOTE] > When multiple approaches are available for improving performance, benchmarking the approaches is usually required to determine which approach yields the best results. From a5766e2355d871b8aa5ee1eb0cb0c3b427d473f3 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 12 Dec 2024 07:10:10 -0500 Subject: [PATCH 6/8] Remove logger step (#34349) --- .../qrcodes-for-authenticator-apps.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps.md index 353b9227a7d6..07d390437e72 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps.md @@ -160,16 +160,6 @@ At the top of the `CookieAuthenticationStateProvider.cs` file, add a `using` sta using System.Text.Json.Serialization; ``` -Inject an `ILogger` to log exceptions in the class: - -```diff -- public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) -- : AuthenticationStateProvider, IAccountManagement -+ public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory, -+ ILogger logger) -+ : AuthenticationStateProvider, IAccountManagement -``` - In the , add the option set to , which avoids serializing null properties: ```diff From 9092b764ec0daab01cb631e7a7788254f543148e Mon Sep 17 00:00:00 2001 From: Robert Haken Date: Thu, 12 Dec 2024 13:23:57 +0100 Subject: [PATCH 7/8] [Blazor] Lazy loading - Complete example - Prevent repeated loads (#34243) --- aspnetcore/blazor/webassembly-lazy-load-assemblies.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/webassembly-lazy-load-assemblies.md b/aspnetcore/blazor/webassembly-lazy-load-assemblies.md index a89174db8f3d..a0054cb6672b 100644 --- a/aspnetcore/blazor/webassembly-lazy-load-assemblies.md +++ b/aspnetcore/blazor/webassembly-lazy-load-assemblies.md @@ -592,16 +592,18 @@ The assembly is assigned to lazyLoadedAssemblies = new(); + private bool grantImaharaRobotControlsAssemblyLoaded; private async Task OnNavigateAsync(NavigationContext args) { try { - if (args.Path == "robot") + if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded) { var assemblies = await AssemblyLoader.LoadAssembliesAsync( new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" }); lazyLoadedAssemblies.AddRange(assemblies); + grantImaharaRobotControlsAssemblyLoaded = true; } } catch (Exception ex) @@ -644,16 +646,18 @@ The assembly is assigned to lazyLoadedAssemblies = new List(); + private bool grantImaharaRobotControlsAssemblyLoaded; private async Task OnNavigateAsync(NavigationContext args) { try { - if (args.Path == "robot") + if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded) { var assemblies = await AssemblyLoader.LoadAssembliesAsync( new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" }); lazyLoadedAssemblies.AddRange(assemblies); + grantImaharaRobotControlsAssemblyLoaded = true; } } catch (Exception ex) From a641dcfe958cd36834eab3514cf6fe1030f5d7b8 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 12 Dec 2024 07:33:10 -0500 Subject: [PATCH 8/8] State preservation (common provider) updates (#34337) --- .../cascading-values-and-parameters.md | 2 +- aspnetcore/blazor/state-management.md | 55 +++++++++++++------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/aspnetcore/blazor/components/cascading-values-and-parameters.md b/aspnetcore/blazor/components/cascading-values-and-parameters.md index 443814c3e863..cbe0baf18366 100644 --- a/aspnetcore/blazor/components/cascading-values-and-parameters.md +++ b/aspnetcore/blazor/components/cascading-values-and-parameters.md @@ -517,4 +517,4 @@ The following `ExampleTabSet` component uses the `TabSet` component, which conta ## Additional resources * [Generic type support: Explicit generic types based on ancestor components](xref:blazor/components/generic-type-support#explicit-generic-types-based-on-ancestor-components) -* [State management: Factor out the state preservation to a common location](xref:blazor/state-management?pivots=server#factor-out-the-state-preservation-to-a-common-location) +* [State management: Factor out state preservation to a common provider](xref:blazor/state-management?pivots=server#factor-out-state-preservation-to-a-common-provider) diff --git a/aspnetcore/blazor/state-management.md b/aspnetcore/blazor/state-management.md index c7734f37e2b0..fb5a2bb7a49f 100644 --- a/aspnetcore/blazor/state-management.md +++ b/aspnetcore/blazor/state-management.md @@ -399,11 +399,17 @@ else :::moniker-end -### Factor out the state preservation to a common location +### Factor out state preservation to a common provider If many components rely on browser-based storage, implementing state provider code many times creates code duplication. One option for avoiding code duplication is to create a *state provider parent component* that encapsulates the state provider logic. Child components can work with persisted data without regard to the state persistence mechanism. -In the following example of a `CounterStateProvider` component, counter data is persisted to `sessionStorage`: +In the following example of a `CounterStateProvider` component, counter data is persisted to `sessionStorage`, and it handles the loading phase by not rendering its child content until state loading is complete. + +The `CounterStateProvider` component deals with prerendering by not loading state until after component rendering in the [`OnAfterRenderAsync` lifecycle method](xref:blazor/components/lifecycle#after-component-render-onafterrenderasync), which doesn't execute during prerendering. + +The approach in this section isn't capable of triggering rerendering of multiple subscribed components on the same page. If one subscribed component changes the state, it rerenders and can display the updated state, but a different component on the same page displaying that state displays stale data until its own next rerender. Therefore, the approach described in this section is best suited to using state in a single component on the page. + +`CounterStateProvider.razor`: :::moniker range=">= aspnetcore-5.0" @@ -430,15 +436,26 @@ else public int CurrentCount { get; set; } - protected override async Task OnInitializedAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + isLoaded = true; + await LoadStateAsync(); + StateHasChanged(); + } + } + + private async Task LoadStateAsync() { var result = await ProtectedSessionStore.GetAsync("count"); CurrentCount = result.Success ? result.Value : 0; - isLoaded = true; + isConnected = true; } - public async Task SaveChangesAsync() + public async Task IncrementCount() { + CurrentCount++; await ProtectedSessionStore.SetAsync("count", CurrentCount); } } @@ -452,7 +469,7 @@ else @using Microsoft.AspNetCore.ProtectedBrowserStorage @inject ProtectedSessionStorage ProtectedSessionStore -@if (isLoaded) +@if (isConnected) { @ChildContent @@ -464,21 +481,32 @@ else } @code { - private bool isLoaded; + private bool isConnected; [Parameter] public RenderFragment ChildContent { get; set; } public int CurrentCount { get; set; } - protected override async Task OnInitializedAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + isConnected = true; + await LoadStateAsync(); + StateHasChanged(); + } + } + + private async Task LoadStateAsync() { CurrentCount = await ProtectedSessionStore.GetAsync("count"); - isLoaded = true; + isConnected = true; } - public async Task SaveChangesAsync() + public async Task IncrementCount() { + CurrentCount++; await ProtectedSessionStore.SetAsync("count", CurrentCount); } } @@ -489,8 +517,6 @@ else > [!NOTE] > For more information on , see . -The `CounterStateProvider` component handles the loading phase by not rendering its child content until state loading is complete. - :::moniker range=">= aspnetcore-8.0" To make the state accessible to all components in an app, wrap the `CounterStateProvider` component around the (`...`) in the `Routes` component with global interactive server-side rendering (interactive SSR). @@ -541,8 +567,7 @@ Wrapped components receive and can modify the persisted counter state. The follo { if (CounterStateProvider is not null) { - CounterStateProvider.CurrentCount++; - await CounterStateProvider.SaveChangesAsync(); + await CounterStateProvider.IncrementCount(); } } } @@ -550,8 +575,6 @@ Wrapped components receive and can modify the persisted counter state. The follo The preceding component isn't required to interact with `ProtectedBrowserStorage`, nor does it deal with a "loading" phase. -To deal with prerendering as described earlier, `CounterStateProvider` can be amended so that all of the components that consume the counter data automatically work with prerendering. For more information, see the [Handle prerendering](#handle-prerendering) section. - In general, the *state provider parent component* pattern is recommended: * To consume state across many components.