HTMX server-side injection
TL;DR: HTMX moves UI state to the server: every interaction is an HTTP request, the server returns HTML fragments, htmx swaps them into the DOM. Security surface: every fragment is unescaped HTML by default (no React-style auto-escape),
hx-targetandhx-swapaccept attacker-controlled CSS selectors via headers,hx-headersJSON gives second-order risk, and the server has to enforce CSRF/auth on what looks like a “partial” request. The model is closer to old jQuery + server templates than to React.
What it is
htmx is a small JS library (~14KB) that adds HTML attributes (hx-get, hx-post, hx-target, hx-swap) to trigger requests and patch the DOM. There’s no client-side framework state — UI is regenerated server-side and sent as HTML. Auth/CSRF/escaping become server-template concerns.
Bug patterns
1. XSS via server templates that emit user data without escaping
The biggest single risk. htmx swaps server response directly into DOM. If the server template emits <div>{{ comment }}</div> and comment is <script>alert(1)</script>, that’s stored XSS. Server template engine must auto-escape (Jinja2 default, Go html/template, Razor) — confirm it does, then audit for |safe / Html.Raw overrides.
- Confirmed by source review of every fragment template.
2. hx-target manipulation
The client can include HX-Target header in the request, which the server is encouraged to use for content negotiation. If the server echoes HX-Target into the response without sanitisation (e.g., for logging or for setting hx-swap-oob), it’s XSS / DOM injection.
3. Out-of-band swap (hx-swap-oob) injection
Server can return <div id="elsewhere" hx-swap-oob="true">…</div> to swap a target other than the requesting element. If a server template includes user input in an OOB swap that targets a privileged element (e.g., the auth status indicator), attacker controls the swap content. Audit every OOB swap for trust boundary.
4. CSRF on every action
htmx requests are normal POST/PUT/DELETE. Server must enforce CSRF as it would for any form. Common mistake: developer treats htmx requests as “internal” because they originate from the same site, and disables CSRF. Anything that triggers a request from hx-trigger="every Xs" or a third-party tab still needs CSRF tokens.
- Hook: htmx supports
hx-headers='{"X-CSRF-Token": "..."}'— config inhtmx.config.includeIndicatorStylesor a wrapper script.
5. hx-headers second-order injection
hx-headers is a JSON attribute on an element. If server templates emit hx-headers='{"X-Token": "{{ token }}"}' without escaping, attacker who controls token injects arbitrary headers — host header attacks, SSRF preconditions, log poisoning.
6. Open swap target via URL
hx-target can be a CSS selector. If a developer renders <button hx-target="{{ target }}" hx-get="/foo">, attacker controls the target — they can swap into body or any privileged element.
7. Server-side template injection
htmx amplifies SSTI risk: every interaction renders a template. A template-injection bug in any fragment route reaches all htmx-driven UI. Same defence as ssti.
8. Out-of-context HTML
htmx swaps don’t preserve the surrounding context. A fragment that’s safe inside <div> may be unsafe inside <script> or <style> if attacker tricks the target. Audit hx-swap settings for any non-innerHTML swap; outerHTML, beforebegin, afterend can poison sibling elements.
9. HX-Redirect and HX-Refresh headers
Server can issue these response headers to trigger client navigation. HX-Redirect: javascript:alert(1) was a CVE in older htmx; current versions sanitise. Pin to a recent htmx (≥ 2.0.4 — earlier had OOB sanitisation gaps; check release notes for current CVEs).
10. Auth check per fragment
Every fragment route is a separate endpoint. Every one needs an auth check. Common bug: main page is auth-gated, but /fragment/comments (called by htmx) isn’t, returning private content unauthenticated.
Audit workflow
- Enumerate every endpoint that returns an HTML fragment (response Content-Type often
text/htmlwith no full<html>envelope). - For each: verify auth/authz, verify CSRF, verify all interpolations are escaped.
- Grep for
hx-swap-oobin templates — every one is a privileged write. - Grep for
|safe/Html.Raw/dangerouslySetInnerHTML/mark_safein fragment templates. - Check
htmx.configoverrides for security-relevant flags (includeIndicatorStyles,allowEval,allowScriptTags,selfRequestsOnly). - Audit
htmx.config.allowEval— if true, server can return<script>and it executes; combined with any XSS surface this is direct code exec.
Hardening checklist
- Set
htmx.config.allowEval = false,htmx.config.allowScriptTags = falsein production config. htmx.config.selfRequestsOnly = trueto block cross-origin htmx requests.- CSP that disallows
unsafe-inlineand uses nonces. - Server-side: escape by default; audit
safeoverrides as if they were dangerous-sink calls. - Treat every fragment endpoint as a full endpoint for auth/CSRF/rate-limit purposes.