DEV Community

Cover image for No More unsafe-inline: Fixing Blazor's Import Map vs CSP Problem
jsakamoto
jsakamoto

Posted on

No More unsafe-inline: Fixing Blazor's Import Map vs CSP Problem

🤔 A Strict CSP Is Hard on Blazor WebAssembly

Have you ever tried to set a strict Content Security Policy (CSP) on a standalone Blazor WebAssembly app? If so, you probably found out that it does not go well.

The reason is the import map. The <script type="importmap"> block inside the fallback page (wwwroot/index.html) is generated automatically when the project is built. You cannot know its hash value in advance, so you cannot write that hash into your policy. In the end, you have to allow unsafe-inline in script-src, and that takes away much of the point of having a CSP at all.

It took me a while, but I finally have an answer for this. I published a NuGet package that solves it. Here it is, the Extensible DevServer ImportMap Extension!

GitHub logo jsakamoto / Toolbelt.Blazor.WebAssembly.ExtensibleDevServer.ImportMapExtension

In a standalone Blazor WebAssembly app, edit a collocated JavaScript module and just reload, with no rebuild, under a strict Content Security Policy.

Blazor WebAssembly Extensible Dev Server - ImportMap Extension

tests NuGet Package

Edit a collocated JavaScript module of a standalone Blazor WebAssembly app and just reload the page No rebuild. And run the whole thing under a strict Content Security Policy, with no 'unsafe-inline'.

⚠️ The problems

Both problems below live in wwwroot/index.html of a standalone Blazor WebAssembly project, once you turn on the .NET 10 SDK's HTML asset placeholder rewriting (OverrideHtmlAssetPlaceholders, more on this in How to use). This package fixes both of them.

Editing a .js file breaks the page until you rebuild

With that setting on, the SDK fingerprints your collocated JavaScript modules and writes an import map into wwwroot/index.html that lists them, along with the digest of each one.

<script type="importmap">{
  "imports": { "./App.razor.js": "./App.8lc58hsgxm.razor.js" },
  "integrity": { "./App.8lc58hsgxm.razor.js": "sha256-BIUnR0qePjnxaXNSj5iBv2jD/L2W6pBFf9oqSxkq+P4=" }
}</script>
Enter fullscreen mode Exit fullscreen mode


💡 The Groundwork Was Already There

This did not come out of nowhere. Before this, I had built the piece that makes it possible. It is a replacement for the dev server of a standalone Blazor WebAssembly project, and it lets you add any middleware you like afterwards.

That article covers the Extensible Dev Server, a drop-in replacement for the standard Microsoft.AspNetCore.Components.WebAssembly.DevServer package, plus an extension that runs on it and merges User Secrets into the appsettings.*.json response. Thanks to the ASP.NET Core Hosting Startup feature, you can inject any middleware into the dev server's HTTP request pipeline afterwards.

So the ground was ready. On top of that extension mechanism, I could finally build and publish this package, the one that fixes the bad relationship between the Blazor import map and CSP.


🔧 How It Works

⚡ During Development, the Dev Server Response Is Rewritten

Let's start with the development scenario. The middleware of this extension captures the fallback page response from the dev server itself. It computes the SHA256 hash of the import map part on the spot. Then it replaces the placeholder sha256-{importmap}, written in the CSP part of that same page, with the real hash value it just computed, and sends the result to the browser.

So all you have to write in your index.html is this.

<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    object-src 'none';
    script-src 'self' 'wasm-unsafe-eval' 'sha256-{importmap}';
    style-src 'self';
    connect-src 'self';" />
Enter fullscreen mode Exit fullscreen mode

The hash of the text inside <script type="importmap"> changes every time the import map changes. But the dev server computes it again every time, so you never have to manage it by hand. Also, no file on disk is touched. Only the content of the HTTP response is rewritten.

🛠️ At Publish Time, an MSBuild Task Does the Same Job

dotnet publish does not involve the dev server. So at publish time, an MSBuild task that ships with this package rewrites the published wwwroot/index.html in the same way. It runs right after the .NET SDK has generated the import map. That timing matters. Everything the SDK produces after that point, such as the pre-compressed .br and .gz copies, then stays consistent with the page you actually ship.

Both scenarios are covered, development and publish.

You may want to send your CSP as an HTTP response header instead of a <meta> tag. For that case, the hash computed at publish time can be written out to a file, so you can feed it to whatever configures your web server.

dotnet publish -p:ImportMapCspHashOutputFile=obj/csp-hash.txt
Enter fullscreen mode Exit fullscreen mode

🚀 How to Use It

🛠️ 1. Add the Packages

Replace the standard dev server package with the Extensible Dev Server, and add this package.

dotnet remove package Microsoft.AspNetCore.Components.WebAssembly.DevServer
dotnet add package Toolbelt.Blazor.WebAssembly.ExtensibleDevServer
dotnet add package Toolbelt.Blazor.WebAssembly.ExtensibleDevServer.ImportMapExtension
Enter fullscreen mode Exit fullscreen mode

🛠️ 2. Make Sure the Import Map Is Generated

Add this to your project file (.csproj).

<PropertyGroup>
  <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

🚀 3. Put the Placeholder in Your CSP

Write 'sha256-{importmap}' in the script-src of the CSP in your index.html. That's it. No extra code is needed.


🎉 Summary

The import map is generated as an inline script, so until now you had to allow unsafe-inline. That problem is now solved from both sides. A dev server extension takes care of it while you develop, and an MSBuild task takes care of it when you publish. Both write the real hash into the CSP of the fallback page.

Now you can protect a standalone Blazor WebAssembly app with a strict CSP, with no unsafe-inline. Give it a try.

If you try it out, or if you have any questions or feedback, feel free to share them in the comments! 👇

❤️ Happy coding!

Top comments (1)

Collapse
 
iqtechsolutions profile image
Ivan Rossouw

The existing publish and Playwright coverage handles the build boundary well. The next boundary I’d test is the final HTML-rewriting hop: fetch the deployed index.html, extract the import-map text exactly as the browser sees it, recompute SHA-256, and verify that the served CSP admits that hash. A CDN minifier can change the inline bytes, while a reverse proxy can inject another CSP policy. Capturing securitypolicyviolation events would also produce a precise release-gate failure instead of only ‘Blazor didn’t start.’ Have you exercised the sample behind a static host or proxy that rewrites HTML or adds its own CSP?