Skip to content
← All posts
· Delta1 Labs Blazor.NETObfuscationWebAssembly

How to obfuscate a Blazor WebAssembly app on .NET 8/9/10 (WebCil)

Modern Blazor WebAssembly wraps your real .NET assemblies in WebCil, fingerprints them and guards them with integrity hashes — so post-publish obfuscators break the app. Here is why, and how to protect a Blazor WASM app the correct way: obfuscate at build time, before the SDK packages WebCil.

To obfuscate a Blazor WebAssembly app on .NET 8, 9 or 10, apply the obfuscation to your app assembly at build time — before the .NET SDK packages it into WebCil — rather than to the finished publish output. Do it that way and the SDK emits correct WebCil, correct fingerprints, correct integrity hashes and correct compression over your already-protected code. Do it the old way, by rewriting the DLLs in the publish folder, and the app simply won’t load.

That “before, not after” detail is the whole game on modern Blazor. Here’s why it changed, and how to get it right.

Why modern Blazor WASM is hard to protect

Blazor WebAssembly runs your real, compiled .NET assemblies in the browser. That has always made it the most exposed .NET target — the actual assemblies stream down to every visitor, who can save them and decompile them back to C#. What changed in recent .NET releases is how those assemblies are packaged, and every one of those changes gets in the way of a naive obfuscation pass.

Assemblies are WebCil wrapped inside .wasm

Since .NET 8, the SDK’s default is WebCil: each managed assembly is wrapped in a small WebAssembly-shaped container and served as a .wasm file. WebCil exists because some corporate proxies, CDNs and host firewalls strip, block or rewrite .dll downloads; wrapping the assembly in a .wasm envelope sidesteps that and loads cleanly through the browser’s WebAssembly pipeline. The practical consequence for protection: there is no plain .dll in the output to feed to an obfuscator. What’s on disk is a WebCil container you’d have to unwrap first.

Filenames are fingerprinted

.NET 8+ fingerprints framework assets with a content hash in the filename — you’ll see something like MyApp.a1b2c3d4e5.wasm in _framework. The fingerprint is derived from the file’s contents, and every reference to the file (in the boot manifest and elsewhere) uses the fingerprinted name. Change the bytes after the fact and the fingerprint is wrong, and everything that points at the old name is now pointing at a file that doesn’t exist.

Integrity is enforced, and there’s no blazor.boot.json to patch

The loader verifies every asset against a recorded integrity (SRI) hash before running it. This is where the old advice really breaks down: on .NET 9 and 10 there is no standalone blazor.boot.json to open and edit — the boot configuration (the resource list and its integrity hashes) is generated by the SDK and embedded in the dotnet.js startup machinery. The manifest you used to patch by hand isn’t sitting there as a file anymore. On top of that, the SDK ships pre-compressed .br (Brotli) and .gz copies of each asset that the runtime prefers.

Why naive post-publish tools break it

Put those together and you can see why “publish, then run an obfuscator over the output” no longer works. To swap in an obfuscated assembly after publish, a tool would have to:

  1. Unwrap the WebCil container back to a raw assembly.
  2. Rewrite the assembly with the obfuscation transforms.
  3. Re-wrap it as WebCil.
  4. Recompute the content-hash fingerprint and rename the file.
  5. Recompute the SRI integrity hash and write it back into the boot config — which may be baked into dotnet.js, not a JSON file.
  6. Regenerate the .br and .gz copies so the runtime doesn’t load a stale original.

Miss any single step and you get an integrity failure or a white screen. And because the format, the fingerprinting scheme and where the manifest lives have all moved between .NET 8, 9 and 10, a post-publish rewriter is chasing a moving target and tends to break on the next SDK bump. It’s the wrong place in the pipeline to intervene.

The correct approach: obfuscate before WebCil packaging

The fix is to move the protection upstream of the packaging. Your app assembly is a perfectly ordinary managed assembly right after the compiler produces it and before the SDK turns it into WebCil. That’s the moment to obfuscate it. Insert an MSBuild step that transforms the compiled assembly at that point, and then let the SDK’s normal publish pipeline do everything it already does — package WebCil, fingerprint, compute integrity, trim, compress — except now it does all of that over your protected code.

The elegant part is that nothing downstream needs patching. The SDK computes the fingerprint and the integrity hash from the obfuscated bytes, so they’re correct by construction. The boot manifest references the right file whether it lives in dotnet.js or a JSON file. The compressed copies are generated from the protected assembly. There is no manifest to repair because you never invalidated it.

This is how Nebula.NET protects Blazor WebAssembly. You add its build integration (a Nebula.Blazor.targets import) to the app project; it hooks the obfuscation in at the right point in the build, ahead of WebCil packaging, so a normal dotnet publish produces a protected app that boots exactly like the original:

dotnet publish -c Release

No separate post-processing command, no manifest surgery, no regenerating compressed copies by hand — the toolchain does the packaging, as it’s designed to, over code that’s already been through obfuscation. See the Nebula.NET docs for the setup and configuration.

What works on WASM — and what can’t

Not every protection technique can run in a browser, and it’s worth being precise about the line.

Works on Blazor WASM — because these are baked statically into the shipped assembly and need nothing special at runtime:

  • Renaming strips type, method and field names, so the decompiled code loses its most readable layer.
  • String encryption clears the giveaway literals — URLs, messages, keys, SQL — out of the assembly.
  • Control-flow flattening rewrites method bodies into a form a decompiler can’t cleanly rebuild into structured C#.

Can’t work on Blazor WASM — because they rely on runtime IL emission:

  • Method encryption and code virtualization rebuild or decrypt a method body while the app runs, which needs Reflection.Emit / dynamic IL generation. The Blazor WebAssembly runtime — like AOT compilation generally — doesn’t allow generating IL at runtime, so those techniques don’t apply here. They’re the right tool on desktop and server targets, not in the browser.

So the honest expectation for Blazor is: renaming, string encryption and control-flow flattening, applied at build time, over correctly-packaged WebCil.

An honest word on client-side code

Obfuscation changes the economics of reverse engineering; it doesn’t repeal the rule that anything running on a user’s machine can, in principle, be inspected there. A determined attacker with enough time can still make progress against any client-side protection. The goal is not “uncrackable” — that doesn’t exist for code you hand to the browser — it’s to raise the cost of reading your logic from trivial (right now, an unprotected Blazor app decompiles back to near-source in minutes) to expensive enough that it isn’t worth it.

That’s also why the split matters: harden your proprietary logic, pricing rules and feature gates in the client, but keep real secrets — API keys, signing keys, authoritative entitlement decisions — on the server, behind an API, never compiled into an assembly you ship to every visitor.

Try it

If you ship Blazor WebAssembly, the fastest way to see the exposure is to open your own published _framework output in a decompiler and read a component you consider proprietary. Then add build-time protection and look again.

Download Nebula.NET to try it on your own build, or read the Nebula.NET documentation for the Blazor WebAssembly setup and the full transform list.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.