Blazor and CSP

This post looks at the a recent fix for Blazor which I think is of massive importance. You can now develop with Blazor in Visual Studio using a strong CSP.

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

History

  • 2024-02-04 Updated CSP, added strict dynamic
  • 2024-01-14 Updated to support CSP nonces

When developing applications, the development environment should be as close as possible to the target production deployment. As a rule, the more these two differ, the more effort you have. You should always develop with HTTPS and you should always use the same CSP in development as in production. (apart from the URLs) This prevents developers adding unsecure scripts and styles before it gets expensive to remove them. This also helps you not deploying unsecure HTTP links, or references to CDNs which cannot be used or using iframes anywhere. All of this reduces the development effort and reduces to overall cost of the project. It is a very bad idea to develop in HTTP and even worse to deploy HTTP.

Visual Studio issue CSP fix:

https://developercommunity.visualstudio.com/t/browserlink-CSP-support-NET-7/10061464

Before this fix, when using a good CSP definition, the hot reload did not work and the console displayed warnings. To remove this you had to use a weak CSP and this is a bad idea because you would need to fix your CSP issues after deploying.

After this fix, you can develop and deploy using the same CSP definition and also develop using hot reload with an active CSP.

How to define good security headers

The best way to secure your session in Blazor is to use the NetEscapades.AspNetCore.SecurityHeaders Nuget package (as well as the tag package for nonces) and add this to the Server part of the Blazor application. Modern secure Blazor applications use the BFF security architecture. By using BFF, no sensitive data is stored in the browser. The NetEscapades.AspNetCore.SecurityHeaders package makes it easy to apply all the required headers in one place.

Once the package is part of the project, you can define the headers for Blazor as follows:

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

        var policy = new HeaderPolicyCollection()
            .AddFrameOptionsDeny()
            .AddContentTypeOptionsNoSniff()
            .AddReferrerPolicyStrictOriginWhenCrossOrigin()
            .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin())
            .AddCrossOriginResourcePolicy(builder => builder.SameOrigin())
            .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp())
            .AddContentSecurityPolicy(builder =>
            {
                builder.AddObjectSrc().None();
                builder.AddBlockAllMixedContent();
                builder.AddImgSrc().Self().From("data:");
                builder.AddFormAction().Self().From(idpHost);
                builder.AddFontSrc().Self();
                builder.AddStyleSrc().Self();
                builder.AddBaseUri().Self();
                builder.AddFrameAncestors().None();

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

            })
            .RemoveServerHeader()
            .AddPermissionsPolicy(builder =>
            {
                builder.AddAccelerometer().None();
                builder.AddAutoplay().None();
                builder.AddCamera().None();
                builder.AddEncryptedMedia().None();
                builder.AddFullscreen().All();
                builder.AddGeolocation().None();
                builder.AddGyroscope().None();
                builder.AddMagnetometer().None();
                builder.AddMicrophone().None();
                builder.AddMidi().None();
                builder.AddPayment().None();
                builder.AddPictureInPicture().None();
                builder.AddSyncXHR().None();
                builder.AddUsb().None();
            });

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

        policy.ApplyDocumentHeadersToAllResponses();

        return policy;
    }
}

In the actual .NET 8 preview version and .NET 7 version, there is an issue preventing Blazor applications using the WasmUnsafeEval. UnsafeEval is still required. It would be nice if we could use a CSP with a nonce but at present, this is not possible with the actual Blazor version. This is something which can be improved.

The headers are applied the to all responses including static files.

var app = builder.Build();

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
}

app.UseSecurityHeaders(
    SecurityHeadersDefinitions.GetHeaderPolicyCollection(env.IsDevelopment(),
        configuration["AzureAd:Instance"]));

app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();

app.UseNoUnauthorizedRedirect("/api");

app.UseAuthentication();
app.UseAuthorization();

Notes

The CSP can still be improved, it is not perfect, but this requires changes in Blazor and the application hosting. At least we can still use the same CSP as in production.

Links

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

https://developercommunity.visualstudio.com/t/browserlink-CSP-support-NET-7/10061464

https://github.com/dotnet/aspnetcore/issues/34428

https://github.com/dotnet/AspNetCore.Docs/pull/29294

https://report-uri.com/home/hash/

6 comments

  1. […] Blazor and CSP [#.NET #.NET Core #ASP.NET Core #Azure #Azure AD #Blazor #CSP #dotnet] […]

  2. […] Blazor and CSP – Damien Bowden […]

  3. Dave · · Reply

    Hi Damien, do you not get an error in blazor.webassemblyjs saying that style-src also needs unsafe-inline?

    mines fails on Q.innerHTML = a || ” ” in

    insertMarkup(e, t, n, r) {
    const o = M(t, n)
    , s = (a = e.frameReader.markupContent(r),
    z(t) ? (ee.innerHTML = a || ” “,
    ee) : (Q.innerHTML = a || ” “,
    Q.content));
    var a;
    let i = 0;
    for (; s.firstChild; )
    x(s.firstChild, o, i++)
    }

    1. Hi Dave, no I do not have this. I will validate this again

      Greetings Damien

Leave a comment

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