Using a CSP nonce in Blazor Web

This article shows how to use a CSP nonce in a Blazor Web application using the InteractiveServer server render mode. Using a CSP nonce is a great way to protect web applications against XSS attacks and other such Javascript vulnerabilities.

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

Notes

The code in this example was built using the example provided by Javier Calvarro Nelson.

https://github.com/javiercn/BlazorWebNonceService

Services and middleware

The Blazor Web application is implemented using the AddInteractiveServerComponents for the InteractiveServer server render mode. The nonce can be used by implementing a nonce service using the CircuitHandler. The nonce service is a scoped service.

builder.Services.AddRazorComponents()
	.AddInteractiveServerComponents();

builder.Services.TryAddEnumerable(ServiceDescriptor
	.Scoped<CircuitHandler, BlazorNonceService>(sp =>
		sp.GetRequiredService<BlazorNonceService>()));

builder.Services.AddScoped<BlazorNonceService>();

The headers are implemented using the NetEscapades.AspNetCore.SecurityHeaders package. The headers are added to the Blazor nonce service using the NonceMiddleware middleware.

app.UseSecurityHeaders(SecurityHeadersDefinitions.GetHeaderPolicyCollection(
	app.Environment.IsDevelopment(),
	app.Configuration["OpenIDConnectSettings:Authority"]));

app.UseMiddleware<NonceMiddleware>();

Setup Security headers

The security headers CSP script tag is setup as best possible for a Blazor Web application. A CSP nonce is used as well as the fallback definitions for older browsers.

.AddContentSecurityPolicy(builder =>
{
    builder.AddObjectSrc().None();
    builder.AddBlockAllMixedContent();
    builder.AddImgSrc().Self().From("data:");
    builder.AddFormAction().Self().From(idpHost);
    builder.AddFontSrc().Self();
    builder.AddBaseUri().Self();
    builder.AddFrameAncestors().None();

    builder.AddStyleSrc()
		.UnsafeInline()
		.Self();

    // due to Blazor
    builder.AddScriptSrc()
		.WithNonce()
		.UnsafeEval() // due to Blazor WASM
		.StrictDynamic()
		.OverHttps()
		.UnsafeInline(); // fallback for older browsers when the nonce is used 
})

Setup Middleware to add the nonce to the state

The NonceMiddleware uses the nonce header created by the security headers package and sets the Blazor nonce service with the value. This is updated on every request.

namespace BlazorWebFromBlazorServerOidc;

public class NonceMiddleware
{
    private readonly RequestDelegate _next;

    public NonceMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, 
          BlazorNonceService blazorNonceService)
    {
        var success = context.Items.TryGetValue(
          "NETESCAPADES_NONCE", out var nonce);

        if (success && nonce != null)
        {
            blazorNonceService.SetNonce(nonce.ToString()!);
        }
        await _next.Invoke(context);
    }
}

Using the nonce in the UI

The BlazorNonceService can be used from the Blazor components in the InteractiveServer render mode. The nonce is applied to all script tags. If the script does not have the correct nonce, it will not be loaded. The GetNonce method reads the nonce value from the BlazorNonceService service.

@inject IHostEnvironment Env
@inject BlazorNonceService BlazorNonceService
@using System.Security.Cryptography;

<!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="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorWebFromBlazorServerOidc.styles.css" rel="stylesheet" />
    <HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
    <Routes @rendermode="InteractiveServer" />

    http://_framework/blazor.web.js
</body>
</html>

@code
{
    /// <summary>
    /// Original src: https://github.com/javiercn/BlazorWebNonceService
    /// </summary>
    [CascadingParameter] HttpContext Context { get; set; } = default!;

    protected override void OnInitialized()
    {
        var nonce = GetNonce();
        if (nonce != null)
        {
            BlazorNonceService.SetNonce(nonce);
        }
    }

    public string? GetNonce()
    {
        if (Context.Items.TryGetValue("nonce", out var item) 
           && item is string nonce and not null)
        {
            return nonce;
        }

        return null;
    }
}

Notes

Nonces can be applied to Blazor Web using the server rendered mode and the BlazorNonceService which implements the CircuitHandler. Thanks the Javier Calvarro Nelson for providing a solution to this. Next would be to find a solution for the AddInteractiveWebAssemblyComponents setup. You should always use a CSP nonce on a server rendered application and only load scripts with the CSP nonce applied to it.

Links

https://github.com/javiercn/BlazorWebNonceService

https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders

6 comments

  1. […] Using a CSP nonce in Blazor Web – Damien Bowden […]

  2. Can this be done in a hosted web-assembly site that uses a host cshtml file in the Server project?eg you can’t use a @code tag there (your “Using the nonce in the UI” section)

    1. Yes, this is exactly how I do it for hosted Blazor applications. Blazor Web applications do not have an host file.

      1. Sorry, by host I mean the file that contains html, head, body, by default wwwrootindex.html in a new VS template. We move that from Client to Server as Pages_Host.cshtml then use app.MapFallbackToFile(“_Host”) in program.cs.

        Now when we do this we can’t add a @code block to that page so was wondering hor the same could be achieved

      2. My bad I’m was still using legacy Blazor Server with a cshtml file. I’ve upgraded it so now everything is in App.Razor and I can add the @code but if you want the nonce in the page in order to add to script/link tags then I just used_nonce = BlazorNonceService.Nonce

Leave a comment

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