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 the Runtime Gets the Token
The runtime does not expect every trigger boundary to carry its own hidden antiforgery field. Instead, the Heimdall runtime 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, the Heimdall runtime 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. Action and Topic Authorization
Heimdall content actions honor ASP.NET Core authorization metadata, and Bifrost topic subscriptions can be gated before a subscribe token is issued. Use [Authorize] and [AllowAnonymous] for actions; use BifrostTopicPolicy or AuthorizeBifrostTopic for topic-level checks. Authorization policy handlers receive a BifrostTopicResource with the topic and HttpContext.
[Authorize(Roles = "Admin")]
[ContentInvocation("admin.refresh")]
public static IHtmlContent RefreshAdminPanel(HttpContext ctx)
{
return AdminPanel.Render(ctx.User);
}
builder.Services.AddHeimdall(options =>
{
options.BifrostTopicPolicy = "BifrostTopic";
options.AuthorizeBifrostTopic = (ctx, topic) =>
ValueTask.FromResult(topic.StartsWith("user:"));
});
public sealed class TopicHandler
: AuthorizationHandler<TopicRequirement, BifrostTopicResource>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
TopicRequirement requirement,
BifrostTopicResource resource)
{
var topic = resource.Topic;
var user = resource.HttpContext.User;
return Task.CompletedTask;
}
}9. Runtime HTML Sanitization
The Heimdall runtime deliberately strips script tags from action responses before applying them to the DOM. It also removes response directives such as invocation, abort, and redirect elements after those instructions have been processed. This is not a full HTML sanitizer, but a targeted protection for runtime DOM insertion.
response HTML received
-> script tags removed
-> invocation / abort / redirect directives processed
-> directive nodes 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.
10. 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 handling11. 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 the Heimdall runtime intercepts or rewrites afterward.
Initial page load:
- normal browser HTML parsing
- not sanitized by the Heimdall runtime
Heimdall response swap:
- sanitized by the Heimdall runtime before insertion12. 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.
13. 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 response14. 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 the Heimdall runtime
-> script tags removed before insertion15. 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 markup16. 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 emit17. 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 removed18. Allowed Targets and Runtime Limits
Heimdall's runtime exposes a switch for out-of-band processing. OOB invocation processing is enabled by default; the switch is useful when an application wants to ignore invocation directives and keep interaction responses limited to the primary target.
// Default:
Heimdall.config.oobEnabled = true
// Optional stricter mode:
Heimdall.config.oobEnabled = false19. 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 surface20. 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 applied21. 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.