htmx Tutorial · Going Further · Lesson 27 of 30
Security
Escape user content, protect against CSRF, stop htmx from processing untrusted HTML, and the htmx security settings.
1. Escape everything users can write
With htmx, the server builds HTML, so cross-site scripting (XSS) protection happens there, in your templates.
Every modern template engine (Jinja2, Django templates, Blade, EJS with <%= %>, Go html/template) escapes by
default. Never turn that off for user content (|safe, {!! !!}, <%- %>).
There's an htmx-specific twist: if an attacker could inject <button hx-delete="/account">, htmx would make it
work. Escaping prevents that too.
<form hx-post="/comments" hx-target="#comments" hx-swap="beforeend">
<input name="text" value='<img src=x onerror="alert(1)"> <button hx-delete="/account">Click</button>' size="50">
<button>Post safely</button>
</form>
<ul id="comments"></ul>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
// server.escape turns < > " ' & into entities, so the comment shows as text and nothing runs.
server.post("/comments", (req) => `<li>${server.escape(req.body.text)}</li>`);
</script>
2. hx-disable for content you can't fully trust
hx-disable on an element stops htmx from processing any hx- attributes inside it, even after swaps. Wrap
user-generated HTML (comments, Markdown output) with it as a second line of defence:
<div class="comment" hx-disable>{{ comment.html }}</div>
3. CSRF tokens
htmx requests carry the user's cookies like any form, so POST, PUT, PATCH and DELETE need CSRF protection. Send
the token with every request using hx-headers on the <body> (it's inherited):
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
Laravel uses X-CSRF-TOKEN; Django uses X-CSRFToken; check your framework's expected header name. Also use
SameSite=Lax (or Strict) cookies.
4. htmx security settings
| Setting | Default in htmx 2 | Purpose |
|---|---|---|
htmx.config.selfRequestsOnly |
true |
only send requests to the page's own domain |
htmx.config.allowScriptTags |
true |
run <script> tags in swapped content (set false if you never need it) |
htmx.config.allowEval |
true |
allow hx-on, trigger filters and js: values, which use eval-like code |
htmx.config.historyCacheSize |
10 |
pages kept in sessionStorage; set 0 for pages with sensitive data |
With a strict Content Security Policy that blocks unsafe-eval, set allowEval: false and avoid hx-on and
filters.
Test yourself
What does the hx-disable attribute do?
Stops htmx from processing hx- attributes inside that elementDisables the element during requestsTurns off htmx for the whole page permanentlyBlocks requests to other domains
Show answer
Answer: Stops htmx from processing hx- attributes inside that element
Use it around user-generated HTML. (To disable buttons during requests, use hx-disabled-elt.)