top of page
Writer's pictureThe Tech Platform

How to Mitigate Security Threats in Interactive Server-Side Blazor?

Blazor is a framework for building interactive web UIs using C# instead of JavaScript. Server-side Blazor, or Blazor Server, is a flavor of Blazor where the application is executed on the server.


In server-side Blazor applications, all UI interactions and updates are handled over a SignalR connection. When an event occurs, such as a button click or a form submission, the information about the action is sent to the server over the connection. The server processes the event, updates the UI if necessary, and sends the changes back to the client to be rendered.


However, with this interactive and stateful nature of server-side Blazor applications comes a range of potential security threats. If not properly addressed these threats could lead to unauthorized access, data leaks, or even denial of service. Therefore, it’s crucial to understand and mitigate these security threats to ensure the safety and reliability of our applications.


How to Mitigate Security Threats in Interactive Server-Side Blazor?

The need to mitigate security threats in interactive server-side Blazor applications arises from several factors:

  1. Persistent Connection: Unlike traditional web applications, server-side Blazor maintains a constant connection with the client. This opens up potential avenues for attackers to exploit this persistent connection.

  2. Stateful Nature: Server-side Blazor applications are stateful, meaning they maintain user state between requests. This statefulness should be managed carefully to prevent unauthorized access to sensitive information.

  3. JavaScript Interoperability (JS Interop): Blazor applications often interact with JavaScript to utilize a wide range of functionalities by JavaScript libraries. However, this interoperability with JavaScript can expose the application to potential security vulnerabilities if not handled properly.

  4. Resource Exhaustion: Since all processing happens on the server, there’s a risk of resource exhaustion if multiple users use the application simultaneously or if a single user makes too many requests.



How to Mitigate Security Threats in Interactive Server-Side Blazor?

Server-side Blazor applications exist within the server’s memory, and when multiple sessions are active, they all reside within the same process. Each session initiates a ‘circuit’ with its dependency injection container scope.

// Startup.cs
services.AddServerSideBlazor();

AddServerSideBlazor adds server-side Blazor services to the service collection during the application startup. This sets up the creation of circuits for each user session.


The scoped services, which could be anything from data access services to business logic services, are unique for each Blazor session.

// Startup.cs
services.AddScoped<MyScopedService>();

In this example, MyScopedService is a service scoped to the circuit. A new instance of MyScopedService is created for each circuit.


The ‘shared state’ in server-side Blazor applications refers to the stateful data processing model. In this model, the server and client maintain a long-lived relationship, and a circuit maintains a persistent state. This circuit can span connections that are also potentially long-lived, allowing for a rich, interactive user experience.

// MyComponent.razor
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

In this example, AuthenticationState is a part of the shared state maintained by the circuit. It is passed down to components via a cascading parameter.


What is a Circuit in Blazor?

A circuit in Blazor is essentially a logical connection between the client and the server. It acts as a two-way communication channel that allows the server and client to exchange information. Each circuit has its scope and maintains its state.  A new circuit is established when a user interacts with a Blazor application. This circuit has its dependency injection container scope, which means any services scoped to this circuit are unique.


This allows each user session to have its unique state, separate from other sessions. The state is maintained as long as the circuit is active, and it includes things like the UI state, data entered by the user, and any other information that the application needs to keep track of between requests.


While singleton services can be used to share state across apps on the same server, this approach is generally not recommended unless done with extreme caution. The reason is, that it can lead to security vulnerabilities, such as the potential leakage of user state across different circuits.

// Startup.cs
services.AddSingleton<MySingletonService>();

In this example, MySingletonService is a singleton service that could be used to share state. However, it should be used with caution to avoid potential security vulnerabilities.


However, there are certain situations where stateful singleton services can be used in Blazor applications, provided they are specifically designed for this purpose. A good example is the use of a singleton memory cache.

// Startup.cs
services.AddMemoryCache();

AddMemoryCache will add a singleton memory cache to the service collection. This cache will store states that can be accessed using a key.


This is acceptable because accessing a given entry in the memory cache requires a key. As long as the keys are not under the control of the users, the state stored in the cache won’t leak across circuits.


Managing shared state in server-side Blazor applications requires careful consideration to ensure security and performance.


Avoid the use of IHttpContextAccessor in Razor Applications

This is an interface provided by ASP.NET Core, which can be used to access the HttpContext of the current request. HttpContext carries all the information about an HTTP request including headers, cookies, session data, and more.

However, in server-side Blazor applications, IHttpContextAccessor should be avoided with interactive rendering as there isn’t a valid HttpContext available during the interactive rendering process. This is because the interactive rendering process happens outside the context of an HTTP request.


You can use IHttpContextAccessor in middleware or services where the HttpContext is available.


Here’s an example of how you might use IHttpContextAccessor in a service:

public class MyService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MyService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetHeaderValue(string headerName)
    {
        return _httpContextAccessor.HttpContext.Request.Headers[headerName].ToString();
    }
}

In this example, MyService is a service that uses the IHttpContextAccessor to access the HttpContext of the current request. The GetHeaderValue method retrieves the value of a specified header from the request.


Use HttpContext in Razor Components

HttpContext can be used in Razor components but with some restrictions. It can be used as a cascading parameter only in statically-rendered root components. This means you can use HttpContext to perform tasks such as inspecting and modifying headers or other properties in the App component (Components/App.razor). However, the value of HttpContext is always null for interactive rendering because, as mentioned earlier, interactive rendering happens outside the context of an HTTP request.


Here’s an example of how you might use HttpContext as a cascading parameter in a Razor component:

@page "/"

@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Components

[CascadingParameter]
public HttpContext HttpContext { get; set; }

<p>@HttpContext?.Request?.Path</p>

Here, HttpContext is used as a cascading parameter in a Razor component. The HttpContext object is used to display the path of the current request. However, remember that HttpContext will be null for interactive rendering.


Important-

In scenarios where the HttpContext is required in interactive components, it’s recommended that data flow via a persistent component state from the server. This means passing the necessary data from the server to the client through a maintained state, rather than trying to access the HttpContext directly. This approach helps to avoid potential security vulnerabilities, such as the potential leakage of user state across different circuits.


Preventing Resource Exhaustion

Resource exhaustion can occur when a client interacts with a server-side Blazor application in a way that causes the server to consume excessive resources. This primarily affects CPU usage, memory, and client connections. It’s important to note that resource exhaustion isn’t necessarily the result of an attack on the system, such as a Denial of Service (DoS) attack.


It can also occur due to high user demand or inefficient use of resources.

  1. CPU Usage

  2. Memory Usage

  3. Client Connections


1. CPU Usage

CPU exhaustion can occur when one or more clients force the server to perform intensive CPU work. For example, consider an app that calculates a Fibonacci number. The amount of work required to reach the answer depends on the sequence length 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.


Blazor apps must include appropriate checks and limits before performing potentially CPU-intensive work. This is a concern for all public-facing apps.


2. Memory Usage

Memory exhaustion can occur when one or more clients force the server to consume a large amount of memory. For example, consider an app with a component that accepts and displays a list of items. Memory-intensive processing and rendering may dominate the server’s memory to the point where the server performance suffers If the Blazor app doesn’t place limits on the number of items allowed or the number of items rendered back to the client.


To prevent this, the app must use a data structure that imposes an item limit on concurrent users. Also, if a paging scheme isn’t used for rendering, the server uses additional memory for objects that aren’t visible in the UI. 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 displayed items.

  • Implement lists or grids that support virtualization for a more advanced rendering scenario. Using virtualization, lists render a subset of items currently visible to the user.


3. 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. Given the persistent nature of the connections and the stateful nature of server-side Blazor apps, connection exhaustion is a greater risk to the app's availability.


To prevent this, the app can require authentication, which naturally limits the ability of unauthorized users to connect to the app. The app can also limit the number of connections per user. This can be accomplished at the app or the server level by using a proxy/gateway in front of the app.


Avoiding Denial of Service (DoS) Attacks

Denial of Service (DoS) attacks are a type of cyber attack where an attacker seeks to make a machine or network resource unavailable to its intended users by temporarily or indefinitely disrupting the services of a host connected to the Internet. In server-side Blazor applications, a DoS attack could involve a client causing the server to exhaust one or more resources, making the application unavailable.


In a server-side Blazor application, the browser sends a UI event to the server over a SignalR connection when a user interacts with the UI. The server then executes the corresponding event handler. For example, if a user clicks a button, the server executes the button’s OnClick event handler. After the event handler finishes executing, the server sends back UI updates to the client over the same SignalR connection. The client then applies these updates to the UI.


The potential for DoS attacks in server-side Blazor applications comes from the fact that these applications maintain a constant connection between the client and the server for each active user session. An attacker could potentially open many connections to the server, causing it to exhaust its resources. Another potential avenue for DoS attacks is through CPU and memory exhaustion. If an attacker can cause the server to perform intensive computations or consume large amounts of memory, this could slow down the server or even cause it to crash.


To protect against DoS attacks, Blazor applications include default limits and rely on other ASP.NET Core and SignalR limits. These limits are set on CircuitOptions and HubConnectionContextOptions.


CircuitOptions include:

  • DisconnectedCircuitMaxRetained: This option sets the maximum number of disconnected circuits the server retains in memory. If a client disconnects, the server retains the circuit in memory for a specified period, allowing the client to re-establish a connection to the same circuit if they reconnect.

  • DisconnectedCircuitRetentionPeriod: This option sets the server to retain disconnected circuits in memory. The server discards the circuit if the client doesn’t reconnect within this period.

  • JSInteropDefaultCallTimeout: This option sets the default timeout for JavaScript interop calls. This timeout prevents a call from waiting indefinitely for a result.

  • MaxBufferedUnacknowledgedRenderBatches: This option sets the maximum number of unacknowledged render batches the server keeps in memory per circuit. If the client doesn’t acknowledge a render batch within a certain period, the server discards it.


HubConnectionContextOptions include:

  • MaximumReceiveMessageSize: This option sets the maximum size of a single incoming hub message. This limit prevents a client from sending excessively large messages to the server.


To further mitigate DoS attacks, you can implement several strategies:

  • Limit the Number of Concurrent Connections: Limit the number of concurrent connections a single client can establish with the server, you can prevent an attacker from exhausting the server’s resources through too many connections.

  • Use Resource Limits: Set limits on the CPU and memory resources the user session can consume. This can prevent an attacker from causing the server to perform excessive computations or consume excessive memory.

  • Use Authentication and Authorization: By requiring users to authenticate, you can limit the ability of anonymous users to consume resources. Additionally, by implementing authorization rules, you can control what resources authenticated users can access and modify.

  • Monitor and Log Activity: Regularly monitor the server’s resource usage and log any suspicious activity. This can help you identify potential DoS attacks and take action before the server’s resources are exhausted.


Securing JavaScript Interop (JS interop)

In server-side Blazor applications, a client interacts with the server through JavaScript Interoperability (JS interop) event dispatching and render completion. JS interop communication goes both ways between JavaScript and .NET:

  1. Browser Events: These are dispatched from the client to the server asynchronously. The server responds asynchronously, rerendering the UI as necessary.

  2. JavaScript Functions Invoked from .NET: For calls from .NET methods to JavaScript, all invocations have a configurable timeout after which they fail, returning an OperationCanceledException to the caller. There’s a default timeout for the calls (CircuitOptions.JSInteropDefaultCallTimeout) of one minute. A cancellation token can be provided to control the cancellation on a per-call.


However, 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 or induce an unintended behavior on the server by returning an unexpected result from the JavaScript function.


To guard against these scenarios, you should wrap JS interop calls within try-catch statements to account for errors that might occur during the invocations. Also, validate data returned from JS interop invocations, including error messages, before taking action.


1. .NET Methods Invoked from the Browser: When a .NET method is exposed to JavaScript, you should 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, and ensure that the user has permission to perform the requested action. Don’t allocate an excessive quantity of resources as part of the .NET method invocation.


For instance, for methods exposed through DotNetObjectReference objects created through dependency injection (DI), the objects should be registered as scoped objects. This applies to any DI service that the app uses. For static methods, avoid establishing a state that can’t be scoped to the client unless the app shares the state by design across all users on a server instance.


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


Events are asynchronous, so multiple events can be dispatched to the server before the app reacts 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.


For example, 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()
    {
        if (count < 3)
        {
            count++;
        }
    }
}

By adding the if (count < 3) { ... } check inside the handler, the decision to increment the 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.


3. 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 the server-side Blazor server.


Securing browser and JavaScript Interop in server-side Blazor applications involves careful management of JS Interop calls, .NET methods, and events. By understanding these concepts and implementing these strategies, you can help protect your server-side Blazor application from potential security threats. I hope this provides a more detailed understanding!


Mitigate Compression Attacks

Side-channel attacks are a type of security vulnerability where an attacker gains additional information by observing the effects of that information rather than by exploiting the software itself. In the context of compression, this can occur because the size of the compressed data can reveal information about the data’s contents.


For example, consider an application that compresses user input before transmitting it. An attacker who can observe the size of the transmitted data might be able to infer information about the user’s input, even if they can’t directly observe the input itself. This is the basis of attacks like CRIME (Compression Ratio Info-leak Made Easy) and BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext), which exploit data compression in HTTPS responses to extract information.


Strategies to Mitigate Compression Attacks

  1. Disable Compression: The simplest way to mitigate compression attacks is to disable compression entirely. However, this can negatively impact the performance of your application, especially if you’re sending large amounts of data.

  2. Use Context-Specific Compression: Instead of compressing all data, you could choose to compress only specific data types that aren’t sensitive. This requires a good understanding of what data your application is handling and how sensitive it is.

  3. Implement Padding: Make all responses the same size by adding padding. This can prevent an attacker from gaining information based on the response size.

  4. Separate Secrets from User Input: By keeping user input and secrets in separate contexts, you can prevent secrets from being inferred from compressed user input.

  5. Limit the Rate of Requests: By limiting how often a client can make requests, you can reduce the effectiveness of attacks that rely on making many requests to gather information.

Conclusion

Mitigating security threats in interactive server-side Blazor applications involves understanding and addressing potential vulnerabilities related to shared state, JavaScript interop calls, resource exhaustion, and DoS attacks. By implementing strategies such as limiting concurrent connections, using resource limits, requiring authentication, and monitoring activity, you can help protect your server-side Blazor application from potential security threats. Remember, building secure applications is just as important as building functional ones.

Comments


bottom of page