Threat mitigation guidance for ASP.NET Core Blazor Server

By Javier Calvarro Nelson

Blazor Server apps adopt a stateful data processing model, where the server and client maintain a long-lived relationship. The persistent state is maintained by a circuit, which can span connections that are also potentially long-lived.

When a user visits a Blazor Server site, the server creates a circuit in the server's memory. The circuit indicates to the browser what content to render and responds to events, such as when the user selects a button in the UI. To perform these actions, a circuit invokes JavaScript functions in the user's browser and .NET methods on the server. This two-way JavaScript-based interaction is referred to as JavaScript interop (JS interop).

Because JS interop occurs over the Internet and the client uses a remote browser, Blazor Server apps share most web app security concerns. This topic describes common threats to Blazor Server apps and provides threat mitigation guidance focused on Internet-facing apps.

In constrained environments, such as inside corporate networks or intranets, some of the mitigation guidance either:

  • Doesn't apply in the constrained environment.
  • Isn't worth the cost to implement because the security risk is low in a constrained environment.

Blazor and shared state

Blazor server apps live in server memory. That means that there are multiple apps hosted within the same process. For each app session, Blazor starts a circuit with its own DI container scope. That means that scoped services are unique per Blazor session.

Warning

We don't recommend apps on the same server share state using singleton services unless extreme care is taken, as this can introduce security vulnerabilities, such as leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they are specifically designed for it. For example, it's ok to use a memory cache as a singleton because it requires a key to access a given entry, assuming users don't have control of what cache keys are used.

Additionally, again for security reasons, you must not use IHttpContextAccessor within Blazor apps. Blazor apps run outside of the context of the ASP.NET Core pipeline. The HttpContext isn't guaranteed to be available within the IHttpContextAccessor, nor is it guaranteed to be holding the context that started the Blazor app.

The recommended way to pass request state to the Blazor app is through parameters to the root component in the initial rendering of the app:

  • Define a class with all the data you want to pass to the Blazor app.
  • Populate that data from the Razor page using the HttpContext available at that time.
  • Pass the data to the Blazor app as a parameter to the root component (App).
  • Define a parameter in the root component to hold the data being passed to the app.
  • Use the user-specific data within the app; or alternatively, copy that data into a scoped service within OnInitializedAsync so that it can be used across the app.

For more information and example code, see ASP.NET Core Blazor Server additional security scenarios.

Resource exhaustion

Resource exhaustion can occur when a client interacts with the server and causes the server to consume excessive resources. Excessive resource consumption primarily affects:

Denial of service (DoS) attacks usually seek to exhaust an app or server's resources. However, resource exhaustion isn't necessarily the result of an attack on the system. For example, finite resources can be exhausted due to high user demand. DoS is covered further in the Denial of service (DoS) attacks section.

Resources external to the Blazor framework, such as databases and file handles (used to read and write files), may also experience resource exhaustion. For more information, see ASP.NET Core Performance Best Practices.

CPU

CPU exhaustion can occur when one or more clients force the server to perform intensive CPU work.

For example, consider a Blazor Server app that calculates a Fibonnacci number. A Fibonnacci number is produced from a Fibonnacci sequence, where each number in the sequence is the sum of the two preceding numbers. The amount of work required to reach the answer depends on the length of the sequence and the size of the initial value. If the app doesn't place limits on a client's request, the CPU-intensive calculations may dominate the CPU's time and diminish the performance of other tasks. Excessive resource consumption is a security concern impacting availability.

CPU exhaustion is a concern for all public-facing apps. In regular web apps, requests and connections time out as a safeguard, but Blazor Server apps don't provide the same safeguards. Blazor Server apps must include appropriate checks and limits before performing potentially CPU-intensive work.

Memory

Memory exhaustion can occur when one or more clients force the server to consume a large amount of memory.

For example, consider a Blazor-server side app with a component that accepts and displays a list of items. If the Blazor app doesn't place limits on the number of items allowed or the number of items rendered back to the client, the memory-intensive processing and rendering may dominate the server's memory to the point where performance of the server suffers. The server may crash or slow to the point that it appears to have crashed.

Consider the following scenario for maintaining and displaying a list of items that pertain to a potential memory exhaustion scenario on the server:

  • The items in a List<MyItem> property or field use the server's memory. If the app allows the list of items to grow unbounded, there's a risk of the server running out of memory. Running out of memory causes the current session to end (crash) and all of the concurrent sessions in that server instance receive an out-of-memory exception. To prevent this scenario from occurring, the app must use a data structure that imposes an item limit on concurrent users.
  • If a paging scheme isn't used for rendering, the server uses additional memory for objects that aren't visible in the UI. Without a limit on the number of items, memory demands may exhaust the available server memory. To prevent this scenario, use one of the following approaches:
    • Use paginated lists when rendering.
    • Only display the first 100 to 1,000 items and require the user to enter search criteria to find items beyond the items displayed.
    • For a more advanced rendering scenario, implement lists or grids that support virtualization. Using virtualization, lists only render a subset of items currently visible to the user. When the user interacts with the scrollbar in the UI, the component renders only those items required for display. The items that aren't currently required for display can be held in secondary storage, which is the ideal approach. Undisplayed items can also be held in memory, which is less ideal.

Blazor Server apps offer a similar programming model to other UI frameworks for stateful apps, such as WPF, Windows Forms, or Blazor WebAssembly. The main difference is that in several of the UI frameworks the memory consumed by the app belongs to the client and only affects that individual client. For example, a Blazor WebAssembly app runs entirely on the client and only uses client memory resources. In the Blazor Server scenario, the memory consumed by the app belongs to the server and is shared among clients on the server instance.

Server-side memory demands are a consideration for all Blazor Server apps. However, most web apps are stateless, and the memory used while processing a request is released when the response is returned. As a general recommendation, don't permit clients to allocate an unbound amount of memory as in any other server-side app that persists client connections. The memory consumed by a Blazor Server app persists for a longer time than a single request.

Note

During development, a profiler can be used or a trace captured to assess memory demands of clients. A profiler or trace won't capture the memory allocated to a specific client. To capture the memory use of a specific client during development, capture a dump and examine the memory demand of all the objects rooted at a user's circuit.

Client connections

Connection exhaustion can occur when one or more clients open too many concurrent connections to the server, preventing other clients from establishing new connections.

Blazor clients establish a single connection per session and keep the connection open for as long as the browser window is open. The demands on the server of maintaining all of the connections isn't specific to Blazor apps. Given the persistent nature of the connections and the stateful nature of Blazor Server apps, connection exhaustion is a greater risk to availability of the app.

By default, there's no limit on the number of connections per user for a Blazor Server app. If the app requires a connection limit, take one or more of the following approaches:

  • Require authentication, which naturally limits the ability of unauthorized users to connect to the app. For this scenario to be effective, users must be prevented from provisioning new users at will.
  • Limit the number of connections per user. Limiting connections can be accomplished via the following approaches. Exercise care to allow legitimate users to access the app (for example, when a connection limit is established based on the client's IP address).
    • At the app level:

      • Endpoint routing extensibility.
      • Require authentication to connect to the app and keep track of the active sessions per user.
      • Reject new sessions upon reaching a limit.
      • Proxy WebSocket connections to an app through the use of a proxy, such as the Azure SignalR Service that multiplexes connections from clients to an app. This provides an app with greater connection capacity than a single client can establish, preventing a client from exhausting the connections to the server.
    • At the server level: Use a proxy/gateway in front of the app. For example, Azure Front Door enables you to define, manage, and monitor the global routing of web traffic to an app and works when Blazor Server apps are configured to use Long Polling.

      Note

      Although Long Polling is supported for Blazor Server apps, WebSockets is the recommended transport protocol. Azure Front Door doesn't support WebSockets at this time, but support for WebSockets is under consideration for a future release of the service.

Denial of service (DoS) attacks

Denial of service (DoS) attacks involve a client causing the server to exhaust one or more of its resources making the app unavailable. Blazor Server apps include some default limits and rely on other ASP.NET Core and SignalR limits to protect against DoS attacks that are set on CircuitOptions.

Blazor Server app limit Description Default
DisconnectedCircuitMaxRetained Maximum number of disconnected circuits that a given server holds in memory at a time. 100
DisconnectedCircuitRetentionPeriod Maximum amount of time a disconnected circuit is held in memory before being torn down. 3 minutes
JSInteropDefaultCallTimeout Maximum amount of time the server waits before timing out an asynchronous JavaScript function invocation. 1 minute
MaxBufferedUnacknowledgedRenderBatches Maximum number of unacknowledged render batches the server keeps in memory per circuit at a given time to support robust reconnection. After reaching the limit, the server stops producing new render batches until one or more batches have been acknowledged by the client. 10

Set the maximum message size of a single incoming hub message with HubConnectionContextOptions.

SignalR and ASP.NET Core limit Description Default
HubConnectionContextOptions.MaximumReceiveMessageSize Message size for an individual message. 32 KB

Interactions with the browser (client)

A client interacts with the server through JS interop event dispatching and render completion. JS interop communication goes both ways between JavaScript and .NET:

  • Browser events are dispatched from the client to the server in an asynchronous fashion.
  • The server responds asynchronously rerendering the UI as necessary.

JavaScript functions invoked from .NET

For calls from .NET methods to JavaScript:

  • All invocations have a configurable timeout after which they fail, returning a OperationCanceledException to the caller.
  • The result of a JavaScript call can't be trusted. The Blazor app client running in the browser searches for the JavaScript function to invoke. The function is invoked, and either the result or an error is produced. A malicious client can attempt to:
    • Cause an issue in the app by returning an error from the JavaScript function.
    • Induce an unintended behavior on the server by returning an unexpected result from the JavaScript function.

Take the following precautions to guard against the preceding scenarios:

  • Wrap JS interop calls within try-catch statements to account for errors that might occur during the invocations. For more information, see Handle errors in ASP.NET Core Blazor apps.
  • Validate data returned from JS interop invocations, including error messages, before taking any action.

.NET methods invoked from the browser

Don't trust calls from JavaScript to .NET methods. When a .NET method is exposed to JavaScript, consider how the .NET method is invoked:

  • Treat any .NET method exposed to JavaScript as you would a public endpoint to the app.
    • Validate input.
      • Ensure that values are within expected ranges.
      • Ensure that the user has permission to perform the action requested.
    • Don't allocate an excessive quantity of resources as part of the .NET method invocation. For example, perform checks and place limits on CPU and memory use.
    • Take into account that static and instance methods can be exposed to JavaScript clients. Avoid sharing state across sessions unless the design calls for sharing state with appropriate constraints.
      • For instance methods exposed through DotNetReference objects that are originally created through dependency injection (DI), the objects should be registered as scoped objects. This applies to any DI service that the Blazor Server app uses.
      • For static methods, avoid establishing state that can't be scoped to the client unless the app is explicitly sharing state by-design across all users on a server instance.
    • Avoid passing user-supplied data in parameters to JavaScript calls. If passing data in parameters is absolutely required, ensure that the JavaScript code handles passing the data without introducing Cross-site scripting (XSS) vulnerabilities. For example, don't write user-supplied data to the Document Object Model (DOM) by setting the innerHTML property of an element. Consider using Content Security Policy (CSP) to disable eval and other unsafe JavaScript primitives.
  • Avoid implementing custom dispatching of .NET invocations on top of the framework's dispatching implementation. Exposing .NET methods to the browser is an advanced scenario, not recommended for general Blazor development.

Events

Events provide an entry point to a Blazor Server app. The same rules for safeguarding endpoints in web apps apply to event handling in Blazor Server apps. A malicious client can send any data it wishes to send as the payload for an event.

For example:

  • A change event for a <select> could send a value that isn't within the options that the app presented to the client.
  • An <input> could send any text data to the server, bypassing client-side validation.

The app must validate the data for any event that the app handles. The Blazor framework forms components perform basic validations. If the app uses custom forms components, custom code must be written to validate event data as appropriate.

Blazor Server events are asynchronous, so multiple events can be dispatched to the server before the app has time to react by producing a new render. This has some security implications to consider. Limiting client actions in the app must be performed inside event handlers and not depend on the current rendered view state.

Consider a counter component that should allow a user to increment a counter a maximum of three times. The button to increment the counter is conditionally based on the value of count:

<p>Count: @count<p>

@if (count < 3)
{
    <button @onclick="IncrementCount" value="Increment count" />
}

@code 
{
    private int count = 0;

    private void IncrementCount()
    {
        count++;
    }
}

A client can dispatch one or more increment events before the framework produces a new render of this component. The result is that the count can be incremented over three times by the user because the button isn't removed by the UI quickly enough. The correct way to achieve the limit of three count increments is shown in the following example:

<p>Count: @count<p>

@if (count < 3)
{
    <button @onclick="IncrementCount" value="Increment count" />
}

@code 
{
    private int count = 0;

    private void IncrementCount()
    {
        if (count < 3)
        {
            count++;
        }
    }
}

By adding the if (count < 3) { ... } check inside the handler, the decision to increment count is based on the current app state. The decision isn't based on the state of the UI as it was in the previous example, which might be temporarily stale.

Guard against multiple dispatches

If an event callback invokes a long running operation asynchronously, such as fetching data from an external service or database, consider using a guard. The guard can prevent the user from queueing up multiple operations while the operation is in progress with visual feedback. The following component code sets isLoading to true while GetForecastAsync obtains data from the server. While isLoading is true, the button is disabled in the UI:

@page "/fetchdata"
@using BlazorServerSample.Data
@inject WeatherForecastService ForecastService

<button disabled="@isLoading" @onclick="UpdateForecasts">Update</button>

@code {
    private bool isLoading;
    private WeatherForecast[] forecasts;

    private async Task UpdateForecasts()
    {
        if (!isLoading)
        {
            isLoading = true;
            forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
            isLoading = false;
        }
    }
}

The guard pattern demonstrated in the preceding example works if the background operation is executed asynchronously with the async-await pattern.

Cancel early and avoid use-after-dispose

In addition to using a guard as described in the Guard against multiple dispatches section, consider using a CancellationToken to cancel long-running operations when the component is disposed. This approach has the added benefit of avoiding use-after-dispose in components:

@implements IDisposable

...

@code {
    private readonly CancellationTokenSource TokenSource = 
        new CancellationTokenSource();

    private async Task UpdateForecasts()
    {
        ...

        forecasts = await ForecastService.GetForecastAsync(DateTime.Now, 
            TokenSource.Token);

        if (TokenSource.Token.IsCancellationRequested)
        {
           return;
        }

        ...
    }

    public void Dispose()
    {
        TokenSource.Cancel();
    }
}

Avoid events that produce large amounts of data

Some DOM events, such as oninput or onscroll, can produce a large amount of data. Avoid using these events in Blazor server apps.

Additional security guidance

The guidance for securing ASP.NET Core apps apply to Blazor Server apps and are covered in the following sections:

Logging and sensitive data

JS interop interactions between the client and server are recorded in the server's logs with ILogger instances. Blazor avoids logging sensitive information, such as actual events or JS interop inputs and outputs.

When an error occurs on the server, the framework notifies the client and tears down the session. By default, the client receives a generic error message that can be seen in the browser's developer tools.

The client-side error doesn't include the callstack and doesn't provide detail on the cause of the error, but server logs do contain such information. For development purposes, sensitive error information can be made available to the client by enabling detailed errors.

Enable detailed errors in JavaScript with:

  • CircuitOptions.DetailedErrors.
  • The DetailedErrors configuration key set to true, which can be set in the app settings file (appsettings.json). The key can also be set using the ASPNETCORE_DETAILEDERRORS environment variable with a value of true.

Warning

Exposing error information to clients on the Internet is a security risk that should always be avoided.

Protect information in transit with HTTPS

Blazor Server uses SignalR for communication between the client and the server. Blazor Server normally uses the transport that SignalR negotiates, which is typically WebSockets.

Blazor Server doesn't ensure the integrity and confidentiality of the data sent between the server and the client. Always use HTTPS.

Cross-site scripting (XSS)

Cross-site scripting (XSS) allows an unauthorized party to execute arbitrary logic in the context of the browser. A compromised app could potentially run arbitrary code on the client. The vulnerability could be used to potentially perform a number of malicious actions against the server:

  • Dispatch fake/invalid events to the server.
  • Dispatch fail/invalid render completions.
  • Avoid dispatching render completions.
  • Dispatch interop calls from JavaScript to .NET.
  • Modify the response of interop calls from .NET to JavaScript.
  • Avoid dispatching .NET to JS interop results.

The Blazor Server framework takes steps to protect against some of the preceding threats:

  • Stops producing new UI updates if the client isn't acknowledging render batches. Configured with CircuitOptions.MaxBufferedUnacknowledgedRenderBatches.
  • Times out any .NET to JavaScript call after one minute without receiving a response from the client. Configured with CircuitOptions.JSInteropDefaultCallTimeout.
  • Performs basic validation on all input coming from the browser during JS interop:
    • .NET references are valid and of the type expected by the .NET method.
    • The data isn't malformed.
    • The correct number of arguments for the method are present in the payload.
    • The arguments or result can be deserialized correctly before invoking the method.
  • Performs basic validation in all input coming from the browser from dispatched events:
    • The event has a valid type.
    • The data for the event can be deserialized.
    • There's an event handler associated with the event.

In addition to the safeguards that the framework implements, the app must be coded by the developer to safeguard against threats and take appropriate actions:

  • Always validate data when handling events.
  • Take appropriate action upon receiving invalid data:
    • Ignore the data and return. This allows the app to continue processing requests.
    • If the app determines that the input is illegitimate and couldn't be produced by legitimate client, throw an exception. Throwing an exception tears down the circuit and ends the session.
  • Don't trust the error message provided by render batch completions included in the logs. The error is provided by the client and can't generally be trusted, as the client might be compromised.
  • Don't trust the input on JS interop calls in either direction between JavaScript and .NET methods.
  • The app is responsible for validating that the content of arguments and results are valid, even if the arguments or results are correctly deserialized.

For a XSS vulnerability to exist, the app must incorporate user input in the rendered page. Blazor Server components execute 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):

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

As part of protecting against XSS attacks, consider implementing XSS mitigations, such as Content Security Policy (CSP).

For more information, see Prevent Cross-Site Scripting (XSS) in ASP.NET Core.

Cross-origin protection

Cross-origin attacks involve a client from a different origin performing an action against the server. The malicious action is typically a GET request or a form POST (Cross-Site Request Forgery, CSRF), but opening a malicious WebSocket is also possible. Blazor Server apps offer the same guarantees that any other SignalR app using the hub protocol offer:

  • Blazor Server apps can be accessed cross-origin unless additional measures are taken to prevent it. To disable cross-origin access, either disable CORS in the endpoint by adding the CORS middleware to the pipeline and adding the DisableCorsAttribute to the Blazor endpoint metadata or limit the set of allowed origins by configuring SignalR for cross-origin resource sharing.
  • If CORS is enabled, extra steps might be required to protect the app depending on the CORS configuration. If CORS is globally enabled, CORS can be disabled for the Blazor Server hub by adding the DisableCorsAttribute metadata to the endpoint metadata after calling MapBlazorHub on the endpoint route builder.

For more information, see Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core.

Click-jacking

Click-jacking involves rendering a site as an <iframe> inside a site from a different origin in order to trick the user into performing actions on the site under attack.

To protect an app from rendering inside of an <iframe>, use Content Security Policy (CSP) and the X-Frame-Options header. For more information, see MDN web docs: X-Frame-Options.

Open redirects

When a Blazor Server app session starts, the server performs basic validation of the URLs sent as part of starting the session. The framework checks that the base URL is a parent of the current URL before establishing the circuit. No additional checks are performed by the framework.

When a user selects a link on the client, the URL for the link is sent to the server, which determines what action to take. For example, the app may perform a client-side navigation or indicate to the browser to go to the new location.

Components can also trigger navigation requests programatically through the use of NavigationManager. In such scenarios, the app might perform a client-side navigation or indicate to the browser to go to the new location.

Components must:

  • Avoid using user input as part of the navigation call arguments.
  • Validate arguments to ensure that the target is allowed by the app.

Otherwise, a malicious user can force the browser to go to an attacker-controlled site. In this scenario, the attacker tricks the app into using some user input as part of the invocation of the NavigationManager.NavigateTo method.

This advice also applies when rendering links as part of the app:

  • If possible, use relative links.
  • Validate that absolute link destinations are valid before including them in a page.

For more information, see Prevent open redirect attacks in ASP.NET Core.

Security checklist

The following list of security considerations isn't comprehensive:

  • Validate arguments from events.
  • Validate inputs and results from JS interop calls.
  • Avoid using (or validate beforehand) user input for .NET to JS interop calls.
  • Prevent the client from allocating an unbound amount of memory.
    • Data within the component.
    • DotNetObject references returned to the client.
  • Guard against multiple dispatches.
  • Cancel long-running operations when the component is disposed.
  • Avoid events that produce large amounts of data.
  • Avoid using user input as part of calls to NavigationManager.NavigateTo and validate user input for URLs against a set of allowed origins first if unavoidable.
  • Don't make authorization decisions based on the state of the UI but only from component state.
  • Consider using Content Security Policy (CSP) to protect against XSS attacks.
  • Consider using CSP and X-Frame-Options to protect against click-jacking.
  • Ensure CORS settings are appropriate when enabling CORS or explicitly disable CORS for Blazor apps.
  • Test to ensure that the server-side limits for the Blazor app provide an acceptable user experience without unacceptable levels of risk.