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.

Create a free account to save this page, take notes and track your progress. Sign in

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.

Escaped user input is just textTry it yourself
<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?

  1. Stops htmx from processing hx- attributes inside that element
  2. Disables the element during requests
  3. Turns off htmx for the whole page permanently
  4. Blocks 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.)

Was this page helpful?