🤔 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!
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
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>
…
💡 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';" />
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
🚀 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
🛠️ 2. Make Sure the Import Map Is Generated
Add this to your project file (.csproj).
<PropertyGroup>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
🚀 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)
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?