Revisiting using a Content Security Policy (CSP) nonce in Blazor

This blog looks at implementing a strong Content Security Policy (CSP) in web applications implemented using Blazor and ASP.NET Core. When implementing CSP, I always recommend using a CSP nonce or at least CSP hashes. If a technical stack does not support CSP nonces, you should probably avoid using this solution when implementing secure and professional web applications.

Code: https://github.com/damienbod/BlazorServerOidc

Older related blogs

Types of Blazor applications

Before implementing a robust Content Security Policy (CSP) in Blazor, it’s essential to identify the specific type of Blazor application you are working with. Blazor offers various forms and render modes, so it’s crucial to select the one that best aligns with your requirements.

  • Blazor Web Server (Interactive Server)
  • Blazor Web WASM (Interactive WebAssembly)
  • Blazor Web Mixed mode (Interactive Auto)
  • Blazor WASM hosted in ASP.NET Core (Razor Page host)
  • Blazor WASM standalone
  • Blazor Server , can be updated to Blazor Web Server (Interactive Server)

I only use Blazor application types, render modes that support a CSP nonce. Currently, only three types of Blazor applications offer this support:

  • Blazor Web Server (Interactive Server)
  • Blazor Web WASM (Interactive WebAssembly)
  • Blazor Web Mixed mode (Interactive Auto)
  • Blazor WASM hosted in ASP.NET Core (Razor Page host)
  • Blazor WASM standalone
  • Blazor Server, can be updated to Blazor Web Server (Interactive Server)

Blazor Web setup

When using the latest version of Blazor, the Interactive Server render mode can be used and the Interactive Auto render mode should be avoided, if security is important in the application. This can be setup using the NetEscapades.AspNetCore.SecurityHeaders Nuget package as follows:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorComponents()
            .AddInteractiveServerComponents();

        builder.Services.AddHttpContextAccessor();

        // ...

        builder.Services.AddSecurityHeaderPolicies()
            .SetDefaultPolicy(SecurityHeadersDefinitions
            .GetHeaderPolicyCollection(oidcConfig["Authority"],
                builder.Environment.IsDevelopment()));

        var app = builder.Build();

        // ...

        app.UseSecurityHeaders();
        app.UseHttpsRedirection();
        app.UseAntiforgery();
        app.UseAuthentication();
        app.UseAuthorization();

        app.MapStaticAssets();
        app.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode();

        app.MapLoginLogoutEndpoints();

        app.Run();
    }
}

Implementing security headers

The NetEscapades.AspNetCore.SecurityHeaders Nuget package can be used to implement security headers in an ASP.NET Core application. This applies security headers to the responses of the different endpoints. One of the headers is the browser CSP header. The CSP nonce is used as recommended by the latest browsers.

namespace BlazorWebApp;

using Microsoft.AspNetCore.Builder;

public static class SecurityHeadersDefinitions
{
    public static HeaderPolicyCollection GetHeaderPolicyCollection(string? idpHost, bool isDev)
    {
        ArgumentNullException.ThrowIfNull(idpHost);

        var policy = new HeaderPolicyCollection()
            .AddFrameOptionsDeny()
            .AddContentTypeOptionsNoSniff()
            .AddReferrerPolicyStrictOriginWhenCrossOrigin()
            .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin())
            .AddCrossOriginResourcePolicy(builder => builder.SameOrigin())
            // #if !DEBUG // remove for dev if using Visual studio development hot reload 
            .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp())
            // #endif
            .AddContentSecurityPolicy(builder =>
            {
                builder.AddObjectSrc().None();
                builder.AddBlockAllMixedContent();
                builder.AddImgSrc().Self().From("data:");
                builder.AddFormAction().Self().From(idpHost);
                builder.AddFontSrc().Self();
                builder.AddStyleSrc().Self().UnsafeInline();
                builder.AddBaseUri().Self();
                builder.AddFrameAncestors().None();

                // #if !DEBUG // remove for Visual studio development
                builder.AddScriptSrc().WithNonce().UnsafeInline();
                // #endif
            })
            .RemoveServerHeader()
            .AddPermissionsPolicyWithDefaultSecureDirectives();

        if (!isDev)
        {
            // maxage = one year in seconds
            policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains();
        }

        return policy;
    }
}

The headers can be added to the services.

builder.Services.AddSecurityHeaderPolicies()
.SetDefaultPolicy(SecurityHeadersDefinitions
.GetHeaderPolicyCollection(oidcConfig["Authority"],
	builder.Environment.IsDevelopment()));

The HttpContextAccessor can be used to get the header from the HTTP context and used to load the scripts and the styles in the UI components. The ImportMap is extended with the nonce.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" nonce="@Nonce" />
    <link rel="stylesheet" href="@Assets["app.css"]" nonce="@Nonce" />
    <link rel="stylesheet" href="@Assets["BlazorWebApp.styles.css"]" nonce="@Nonce" />
    
    <ImportMap AdditionalAttributes="@(new Dictionary<string, object>() { { "nonce", Nonce ?? "" }})" />

    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes @rendermode="InteractiveServer" />
    <script src="_framework/blazor.web.js" nonce="@Nonce"></script>
</body>
</html>
@code
{
    public string? Nonce => HttpContextAccessor?.HttpContext?.GetNonce();
    [Inject] private IHttpContextAccessor? HttpContextAccessor { get; set; }
}

Visual Studio debugging

When debugging using Visual Studio, it adds two scripts which are blocked by default and should be blocked. This is a script attack and should be blocked in any deployments.

If you would like to allow this in Visual Studio debugging, you can use the #if !DEBUG in the SecurityHeadersDefinitions class to allow the following injected scripts:

<!-- Visual Studio Browser Link -->
<script type="text/javascript" src="/_vs/browserLink" async="async" id="__browserLink_initializationData" data-requestId="59852cf479154d149a3db2064a0722e6" data-requestMappingFromServer="false" data-connectUrl="http://localhost:63449/fd8b98433c6f43259bb7df9563900638/browserLink"></script>
<!-- End Browser Link -->
<script src="/_framework/aspnetcore-browser-refresh.js"></script>

Notes

Using CSP nonces makes it easy to apply, update and maintain an application and use a strong CSP in all environments. I use this in dev, test and production setups. Any web technical stacks which do not support CSP nonces should probably be avoided when building professional web applications. Blazor InteractiveServer render mode has a good solution.

Links

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/

https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models

2 comments

  1. […] Revisiting using a Content Security Policy (CSP) nonce in Blazor (Damien Bowden) […]

  2. Unknown's avatar

    […] Revisiting using a Content Security Policy (CSP) nonce in Blazor […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.