htmx allows you to define logic directly in your DOM. This has a number of advantages, the largest being Locality of Behavior, which makes your system easier to understand and maintain.
A concern with this approach, however, is security: since htmx increases the expressiveness of HTML, if a malicious user is able to inject HTML into your application, they can leverage this expressiveness of htmx to malicious ends.
Rule 1: Escape All User Content
The first rule of HTML-based web development has always been: do not trust input from the user. You should escape all 3rd party, untrusted content that is injected into your site. This is to prevent, among other issues, XSS attacks.
There is extensive documentation on XSS and how to prevent it on the excellent OWASP Website, including a Cross Site Scripting Prevention Cheat Sheet.
The good news is that this is a very old and well understood topic, and the vast majority of server-side templating languages support automatic escaping of content to prevent just such an issue.
That being said, there are times people choose to inject HTML more dangerously, often via some sort of raw()
mechanism in their templating language. This can be done for good reasons, but if the content being injected is coming
from a 3rd party then it must be scrubbed, including removing attributes starting with hx- and data-hx, as well as
inline <script> tags, etc.
If you are injecting raw HTML and doing your own escaping, a best practice is to whitelist the attributes and tags you allow, rather than to blacklist the ones you disallow.
htmx Security Tools
Of course, bugs happen and developers are not perfect, so it is good to have a layered approach to security for your web application, and htmx provides tools to help secure your application as well.
Let’s take a look at them.
hx-ignore
The first tool htmx provides to help further secure your application is the hx-ignore
attribute. This attribute will prevent processing of all htmx attributes on a given element, and on all elements within
it. So, for example, if you were including raw HTML content in a template (again, this is not recommended!) then you
could place a div around the content with the hx-ignore attribute on it:
<div hx-ignore> <%= raw(user_content) %> </div>
And htmx will not process any htmx-related attributes or features found in that content. This attribute cannot be
disabled by injecting further content: if an hx-ignore attribute is found anywhere in the parent hierarchy of an
element, it will not be processed by htmx.
CSP Options
Browsers also provide tools for further securing your web application. The most powerful tool available is a Content Security Policy. Using a CSP you can tell the browser to, for example, not issue requests to non-origin hosts, to not evaluate inline script tags, etc.
CSP can be set via an HTTP header or a <meta> tag. HTTP headers are preferred — <meta> tags
do not enforce all directives and scripts that appear before the <meta> tag in the document are
not covered by it:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<nonce>'
A full discussion of CSPs is beyond the scope of this document, but the MDN Article provides a good jumping-off point for exploring this topic.
Controlling Cross-Origin Requests
htmx defaults htmx.config.mode to "same-origin", which causes the
browser to reject any cross-origin fetch — even if an attacker injects an hx-get pointing elsewhere.
This setting is enforced globally: any mode value in a per-element hx-config attribute is ignored and
reset to the global config value. This means injected markup like
hx-config='{"mode":"cors"}' cannot widen request scope.
If your application legitimately needs CORS (e.g. an API on a different subdomain):
- Set the mode globally:
htmx.config.mode = "cors"; - Lock down reachable origins with
connect-src:<meta http-equiv="Content-Security-Policy" content="connect-src 'self' https://api.example.com">
With both in place, htmx can reach your API but injected target URLs to other origins are blocked by CSP.
hx-csp Extension
For sites using CSP script nonces, the hx-csp extension provides deep integration:
- Gates all htmx attribute processing behind a per-request nonce, blocking injected htmx attributes
- Automatically creates a
'htmx'Trusted Types policy so only htmx can write HTML into DOM sinks - Replaces
new Function()eval with nonce-based script injection whensafeEval:trueis set, removing the need forunsafe-eval
See the hx-csp extension docs for full setup instructions.
htmx & Eval
htmx uses new Function() for some optional features:
- Event filters
- The
hx-onattribute - Attribute values starting with
js:orjavascript:
All of these are optional. If you don’t use them you can omit unsafe-eval from your CSP entirely.
If you do use these features, the hx-csp extension with safeEval:true replaces
new Function() with nonce-based script injection, enabling them without unsafe-eval.
CSP & Inline Styles
htmx injects its indicator CSS using Constructable Stylesheets (document.adoptedStyleSheets), which are not subject to style-src CSP restrictions.
The one area to be aware of is morph swaps when used alongside JS frameworks like Alpine that set style attributes via JavaScript. During morph, htmx’s __copyAttributes reads all attributes from the new element and copies them to the old one — including any style attributes set by the framework. Under a strict style-src policy without 'unsafe-inline', this setAttribute("style", ...) call will produce a CSP violation.
Add "style" to morphIgnore to skip it:
<meta name="htmx-config" content='{"morphIgnore":["data-htmx-powered","style"]}'>
Class-based CSS transitions continue to work normally.
CSRF Prevention
The assignment and checking of CSRF tokens are typically backend responsibilities, but htmx can support returning the
CSRF token automatically with every request using the hx-headers attribute. The attribute needs to be added to the
element issuing the request or one of its ancestor elements. This makes the html and body elements effective
global vehicles for adding the CSRF token to the HTTP request header, as illustrated below.
<html lang="en" hx-headers='{"X-CSRF-TOKEN": "CSRF_TOKEN_INSERTED_HERE"}'> : </html>
The above elements are usually unique in an HTML document and should be easy to locate within templates.