Hacking htmx applications
With the normal flow of frontend frameworks moving from hipster to mainstream in the coming few months, during a test, you bump into this strange application that receives HTML with `hx-` attributes in responses. Congrats, you are testing your first htmx application, let me give you the building blocks to play with for testing this type of application.
The only way htmx will be different from other web applications is how HTML is assembled and interacted with so I’ll only talk about XSS attacks but all other attacks apply as usual.
Background
Unlike frameworks like Vue or React htmx does not concern itself about creating HTML as much as swapping in fragments from responses and extending HTML’s functionality with more hypertext tools allowing frontend developers to use mostly only HTML for fronted interactions.

In general most of the frontend logic will be in the HTML, especially in htmx specific attributes. In htmx all attributes have 2 versions, hx-<attribute_name> and data-hx-<attribute_name> they are functionally identical, from now on I’ll only use hx-<attribute_name>. In htmx a tag with a hx-<verb> (e.g. hx-get) attribute can trigger a (ajax)request and the response is an HTML that gets swapped in based on css selectors defining the target of the swap (by default the triggering node that made the call) and swapping logic (e.g. innerHTML swapping the content of the target node while, outerHTML would swap the whole node). With the philosophy of swapping you can expect that full page loads will be rare so if you manage to inject javascript you can expect your code to stick around for a while.
In htmx escaping is expected to be done on server side by design and not by the framework itself. If the response includes anything that is an unescaped XSS payload your work is done.
Usually DOM based XSS will be less of a concern since control-flow and data is passed in tags and attributes htmx itself does not read location other than to fix up history. Developers of course can still include references to location attributes in vulnerable sinks in javascript parts.
The library uses xhr to make requests for fragments, which does follow redirects meaning an open redirect in htmx is effectively an XSS. Even if something is a fire-and-forget CORS Post or normally benign JSON request/response, if an attacker can control the response (or just add headers) they can trigger arbitrary script loads.
The framework itself is built lightweight, likely you will bump into extensions, that you can find based on hx-ext attributes on tags.
While it is quite foreign to the philosophy of htmx but it can handle JSON responses (and can make requests) by rendering the response into client side templates, meaning values can and should be escaped by the templating language used.
The library does not differentiate between HTML and JSON responses. If the client-side-templates extension is enabled the framework will try to parse responses as JSON first and apply templates, and if that does not work (JSON is broken or template rendering runs into issues) it will go on and try to do a swap regardless. If you manage to break/remove templates a JSON response {“id”: “<script>maliciousness</script>”} will be swapped in as is and trigger the payload. This design also means the framework disregards both content-type and content-length headers which could come handy in CRLF injection scenarios as well.
Things that you cannot do:
- You cannot directly target and swap an attribute of a node (to trigger XSS) only nodes themselves.
- As mentioned resource loads (like images) are normally handled by the browser and not htmx so you cannot use them.
New javascript sinks
Htmx introduces a number of attributes that work as javascript sinks, if user input gets here without escaping (or just with usual HTML templating escaping) you can find htmx specific XSS.
- The attributes hx-on (for htmx event handlers) and hx-vars (way to change request behavior) are straight up evaluated.
- For hx-headers, hx-vals, hx-request are evaluated IF the string starts with js: or javascript:
- The attribute hx-trigger can include arbitrary javascript e.g. in the following format hx-trigger=load[<arbitrary js>]
- If the include-vals extension is present, values of the include-vals attribute gets into the following context: eval(“({“ + includeVals + “})”). Fixing up the context properly will also allow javascript execution here (e.g. something like “val: <arbitrary js function>”
Controlling behavior htmx way
Since we are trying to trigger XSS and our way in is through responses to affect the DOM.
Response Headers
Changing response handling behavior can be done through response headers (but check the source some of them are not documented). Look for user input getting into any of these headers or straight up response header injection. Most interesting ones:
- Controlling HX-Location allows you to force htmx to make a request to the location and swap the response into the top level htmx element effectively resulting in an XSS (don’t forget to set CORS (or use something like https://allorigins.win/) on the server where you want to load the content from). Setting the header to { “path”:”<somepath>:”, “target”:”<css selector>”} would swap the response into a specific location, this way you can do it largely unnoticed. Location without “/” is translated into local path while “//” is absolute using the same protocol.
- HX-Reselect will allow you to select a subset of the elements from the response to swap in while HX-Reswap will allow you to define where to swap them in. More on this later.
- HX-Trigger would allow you to trigger event handlers in case you find an injection in them
- HX-Redirect will redirect; however, this is a navigation so it just results in a plain old open redirect
Controlling swapping with attributes
There is a special element type in responses, the out of bounds swap signified by the hx-swap-oob attribute. This element can define how and where it will get swapped in, something like “hx-swap-oob=innerHTML:script” would swap in the content of the element into script tags allowing you to cross contexts potentially (HTML element -> Javascript execution). Unfortunately this specific attack does not work generally since existing script tags don’t normally get reevaluated unless for example the tag was empty (also without src set) or you can trigger a reevaluation somehow (e.g. pushing the DOM to history and recovering it).
Exploiting the control of swapping is highly implementation specific, but I’ll leave you with another generic trick: if the application uses client side templates extension, those templates are functionally javascript sinks as well. You can change the template to remove escaping, change the template to just execute your javascript in the template, delete or break the template to have you unescaped JSON response be picked up as HTML.
In general you can think about swap type shenanigans if you can affect one of the following:
- The hx-swap-oob attribute is powerful because you can affect both the type and target
- The hx-target attribute can change what you will swap out (like the html tag including the client side template) and HX-Retarget
Controlling requests
As we established earlier, making requests to untrusted domains or URLs controllable by an attacker in general could result in XSS. This means you should also look at user input or tainted values getting into hx-<verb> attributes. Just as with headers, missing leading slash is resolved to local path, and ‘//’ is resolved as a request to an absolute path using the same protocol.
TLDR of pentesting an htmx app for XSS
- When enumerating the application make sure to interact with everything that has (hx-<verb> attributes) and take note of extensions that are loaded (hx-ext)
- Check to see if there is oldschool XSS based on responses. In general if the responses includes your attack vector without escaping it will execute
- Check to see if you can interact with any of the new javascript sinks, if the developers just use regular HTML escaping, these are injectable
- You will most likely see much heavier use of event handlers, check for user input getting into any of the old school or htmx specific handlers
- Check for open redirects since most likely you can turn them into XSS by redirecting to a page controlled by you and doing an OOB swap.
- Check if you can affect the paths in any of the hx-verb attributes or HX-<header> response headers to cause redirects to a page/site you can control
- Check if any user input gets in hx-target/hx-oob-swap attributes (especially if client side templating extension is included) or HX-Reswap headers
- Check for all requests made by the framework (search for HX-<header> format headers in the requests to see which are framework/browser native requests), see if there are any domains that should not be trusted or would not be trusted to load a javascript dependency from.
- Check for open redirects via HX response headers
Authors
The research was done in collaboration with Mark Charest, thanks Mark!