Security
Heimdall is HTML-first and server-driven, but that does not reduce the need for security boundaries. This page explains the core ones: antiforgery, trusted markup, runtime response handling, and what Heimdall intentionally sanitizes or does not sanitize.
The important idea is that Heimdall keeps the browser small, but it does not make security disappear. It makes the trust boundaries easier to reason about.
1. Security in the Heimdall Model
Heimdall actions are still server-side HTTP endpoints. They accept requests, bind payload, and return HTML. That means normal web security concerns still apply: request authenticity, output trust, and the distinction between safe application-owned markup and untrusted content.
Browser interaction
-> Heimdall request
-> server action
-> HTML response
-> DOM update
Security boundaries still matter at every step2. Why Antiforgery Is Required
Heimdall actions are invoked with same-origin requests and mutate or reveal application state through ordinary HTTP calls. Because of that, Heimdall requires ASP.NET Core antiforgery protection for its action pipeline.
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.UseHeimdall();3. What the Server Actually Enforces
The Heimdall content endpoint validates antiforgery before the action is invoked. If the token is missing or invalid, the request is rejected before payload binding and before any application action runs.
var antiforgery = ctx.RequestServices.GetRequiredService<IAntiforgery>();
await antiforgery.ValidateRequestAsync(ctx);That makes antiforgery part of the actual request gate, not just a documentation recommendation.
4. How Heimdall.js Gets the Token
The runtime does not expect every trigger boundary to carry its own hidden antiforgery field. Instead, heimdall.js fetches a request token from the Heimdall security endpoint, caches it client-side, and sends it with action requests.
GET /__heimdall/v1/csrf
-> requestToken returned
-> runtime caches token
-> token sent on later Heimdall requests5. Why This Is Better Than Treating Heimdall as 'Just AJAX'
A common mistake with HTML-over-the-wire libraries is to think that because the transport returns HTML rather than JSON, request authenticity matters less. It does not. Heimdall actions are still stateful server endpoints and should be protected the same way any other same-origin application endpoint is protected.
6. Retry Behavior on Token Failure
The runtime also treats suspected CSRF or antiforgery failures as a recoverable token problem once. If a request comes back as a 400 response mentioning CSRF or antiforgery, heimdall.js clears its cached token, fetches a fresh one, and retries the request one time.
request fails with 400
-> response suggests csrf / antiforgery failure
-> cached token cleared
-> token fetched again
-> request retried once7. Bifrost / SSE Uses the Same Security Story
Heimdall applies antiforgery not only to content actions but also to the Bifrost subscribe-token endpoint. That means the browser must prove same-origin authenticity before it can obtain the short-lived token used to subscribe to a topic.
GET /__heimdall/v1/bifrost/token?topic=...
+ antiforgery validation
-> short-lived subscribe token returned8. Runtime HTML Sanitization
Heimdall.js deliberately strips script tags from action responses before applying them to the DOM. It also removes invocation elements from the main response body after they have been processed as OOB instructions. This is not a full HTML sanitizer, but a targeted protection for runtime DOM insertion.
response HTML received
-> script tags removed
-> OOB invocation nodes processed / removed
-> sanitized HTML applied by swapThis is an important guardrail because the runtime is inserting returned HTML into a live document rather than navigating to a whole new page.
9. What Gets Sanitized
In practical terms, Heimdall’s runtime sanitization applies to HTML coming back from Heimdall interaction responses: normal action responses, error responses that are surfaced back through the runtime, and OOB payload fragments before they are inserted.
Sanitized by the runtime:
- action response HTML
- OOB payload fragments
- error HTML flowing through Heimdall response handling10. What Does Not Get Sanitized
The runtime does not sanitize the initial page load HTML. A full page response is treated as ordinary server-rendered application output. If the server sends a script tag on initial page render, that is a normal page-level trust decision, not something Heimdall.js intercepts or rewrites afterward.
Initial page load:
- normal browser HTML parsing
- not sanitized by heimdall.js
Heimdall response swap:
- sanitized by heimdall.js before insertion11. Why That Distinction Exists
An initial page load is already inside the ordinary server-rendering trust model of the application. Heimdall is not acting as a browser sandbox for full page HTML. Its sanitization exists specifically for runtime-inserted response content that would otherwise be injected into an already-live document.
12. Trusted Markup on the Server
The StaticAssets helper highlights an important server-side trust boundary. It returns IHtmlContent and writes raw markup directly to the response without encoding. This is intentionally treated as trusted markup by the server.
container.Add(StaticAssets.Get("fragments/home/hero.html"));
// Writes raw markup directly into the response13. Server Trust vs Runtime Insertion
Trusted markup on the server does not automatically mean unrestricted DOM insertion on the client. If markup produced by StaticAssets is returned through a Heimdall content invocation, it will still pass through the runtime's response handling before being inserted into the DOM.
StaticAssets -> server emits raw HTML
If used in full page render:
-> sent directly to browser
-> normal page trust model applies
If used in Heimdall response:
-> response handled by heimdall.js
-> script tags removed before insertion14. Why TrustedMarkup Is Contextual
TrustedMarkup is safe when the file is application-owned and treated like source code. However, its behavior depends on how the markup is delivered. Server-side trust controls encoding, while the runtime controls insertion behavior when the markup is returned through Heimdall.
Safe usage:
- app-owned fragments
- known markup shipped with the app
Context matters:
- full page render -> no runtime sanitization
- Heimdall response -> runtime sanitization applies
Not safe:
- user HTML
- untrusted third-party markup15. Templates Do Not Remove the Need for Trust Decisions
The same idea applies to templates such as Scriban. A template is only as safe as its inputs and how you choose to render its output. Heimdall does not treat 'template output' as automatically trusted or automatically untrusted. That remains a server-side decision.
Template source + data
-> rendered output
-> server decides whether that output is safe to emit16. OOB Safety Boundaries
Out-of-band updates are powerful because one response can update multiple DOM regions. The runtime therefore treats invocation payloads carefully: scripts are stripped from the payload fragment before the OOB swap is applied, and invocation wrappers are removed after processing.
OOB response
-> invocation target resolved
-> payload fragment extracted
-> script tags removed
-> target updated
-> invocation node removed17. Allowed Targets and Runtime Limits
Heimdall’s runtime also exposes configuration for restricting which selectors OOB updates are allowed to target. That is not a substitute for server trust, but it can be a useful defense-in-depth boundary in applications that want to narrow the set of valid OOB destinations.
Heimdall.config.oobAllowedTargets = [...]18. What Security Page Loads Should Teach
The practical lesson is that Heimdall security is not one feature. It is a combination of request authenticity, trusted server rendering, careful runtime insertion behavior, and clear boundaries around what content is considered safe to emit.
Request authenticity -> antiforgery
Server-owned markup -> trust boundary
Runtime insertion -> script stripping
App architecture -> smallest possible trust surface19. Common Mental Model
A useful way to think about Heimdall security is that the server remains the owner of HTML truth, while the runtime is careful about how later HTML is inserted into an already-running page.
Initial page HTML
-> normal server trust boundary
Later Heimdall HTML
-> runtime insertion boundary
-> additional sanitization applied20. Why This Fits Heimdall
Heimdall stays honest about security because it does not pretend HTML-over-the-wire is magically safer than other web programming models. It uses ordinary ASP.NET Core antiforgery, keeps trust decisions on the server, and adds runtime protections specifically where dynamic HTML insertion needs them.
ASP.NET Core security primitives
+ explicit trust boundaries
+ runtime insertion guardrails
-> secure HTML-first interaction modelNext Steps
With the core security model in place, the main conceptual surface of Heimdall is complete. From here, the most useful follow-up is usually tightening examples, polishing reference links between pages, and refining real application patterns.