Heimdall Docs

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 step

2. 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 requests

5. 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.

Heimdall actions are still POST requests.
Those requests may change state or expose sensitive UI.
Same-origin alone is not enough.
Antiforgery remains the correct protection boundary.

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 once

7. 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 returned

8. 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 swap

This 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 handling

10. 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 insertion

11. 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.

Full page render is a server trust decision.
Heimdall response insertion is a runtime trust boundary.
The runtime protects the insertion boundary, not the entire web application.

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 response

13. 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 insertion

14. 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 markup

15. 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 emit

16. 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 removed

17. 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 surface

19. 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 applied

20. 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 model

Next 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.